@object-ui/runner 3.1.5 → 3.3.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/CHANGELOG.md +34 -0
- package/README.md +20 -1
- package/dist/assets/AdvancedChartImpl-CDghlfRH.js +7 -0
- package/dist/assets/BarChart-B099cwDN.js +33 -0
- package/dist/assets/ChartImpl-DGIwAPy6.js +1 -0
- package/dist/assets/KanbanEnhanced-DE0Kx_tP.js +4 -0
- package/dist/assets/KanbanImpl-B4kH8R31.js +1 -0
- package/dist/assets/ResponsiveContainer-1FrXh8mR.js +4 -0
- package/dist/assets/index-B7YzvfS-.css +1 -0
- package/dist/assets/index-BABnUGLB.js +97 -0
- package/dist/assets/sortable.esm-B0an77E6.js +5 -0
- package/dist/index.html +3 -3
- package/package.json +42 -18
- package/.turbo/turbo-build.log +0 -25
- package/dist/assets/AdvancedChartImpl-BXPkj4Gu.js +0 -7
- package/dist/assets/BarChart-B-2dl14E.js +0 -3
- package/dist/assets/ChartImpl-rn7h4RQH.js +0 -1
- package/dist/assets/KanbanEnhanced-C06Lh7Lp.js +0 -4
- package/dist/assets/KanbanImpl-H0W5xMX2.js +0 -1
- package/dist/assets/Tooltip-vqI0Da0Z.js +0 -34
- package/dist/assets/index-BfeSbj6K.js +0 -96
- package/dist/assets/index-DyZw8nvD.css +0 -2
- package/dist/assets/sortable.esm-C55isRg_.js +0 -5
- package/index.html +0 -13
- package/postcss.config.js +0 -14
- package/src/App.tsx +0 -157
- package/src/LayoutRenderer.tsx +0 -320
- package/src/index.css +0 -111
- package/src/lib/MetadataLoader.ts +0 -103
- package/src/lib/mockDataSource.ts +0 -56
- package/src/main.tsx +0 -18
- package/src/plugin-integration.test.ts +0 -64
- package/vite.config.ts +0 -37
package/src/LayoutRenderer.tsx
DELETED
|
@@ -1,320 +0,0 @@
|
|
|
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 React from 'react';
|
|
10
|
-
import type { AppSchema } from '@object-ui/types';
|
|
11
|
-
import * as LucideIcons from 'lucide-react';
|
|
12
|
-
import {
|
|
13
|
-
DropdownMenu,
|
|
14
|
-
DropdownMenuContent,
|
|
15
|
-
DropdownMenuGroup,
|
|
16
|
-
DropdownMenuItem,
|
|
17
|
-
DropdownMenuLabel,
|
|
18
|
-
DropdownMenuSeparator,
|
|
19
|
-
DropdownMenuTrigger,
|
|
20
|
-
DropdownMenuShortcut,
|
|
21
|
-
Avatar,
|
|
22
|
-
AvatarImage,
|
|
23
|
-
AvatarFallback,
|
|
24
|
-
Collapsible,
|
|
25
|
-
CollapsibleContent,
|
|
26
|
-
CollapsibleTrigger
|
|
27
|
-
} from '@object-ui/components';
|
|
28
|
-
|
|
29
|
-
interface LayoutRendererProps {
|
|
30
|
-
app: AppSchema;
|
|
31
|
-
children: React.ReactNode;
|
|
32
|
-
currentPath?: string;
|
|
33
|
-
onNavigate?: (path: string) => void;
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
// Helper to resolve icon from string name (e.g. "bar-chart" -> "BarChart")
|
|
37
|
-
const getIcon = (name?: string) => {
|
|
38
|
-
if (!name) return null;
|
|
39
|
-
|
|
40
|
-
// 1. Try direct match (e.g. "Home")
|
|
41
|
-
if ((LucideIcons as any)[name]) return (LucideIcons as any)[name];
|
|
42
|
-
|
|
43
|
-
// 2. Try PascalCase (e.g. "bar-chart" -> "BarChart")
|
|
44
|
-
const pascalName = name.split('-').map(part => part.charAt(0).toUpperCase() + part.slice(1)).join('');
|
|
45
|
-
if ((LucideIcons as any)[pascalName]) return (LucideIcons as any)[pascalName];
|
|
46
|
-
|
|
47
|
-
return LucideIcons.Circle; // Fallback
|
|
48
|
-
};
|
|
49
|
-
|
|
50
|
-
const NavItem = ({ item, currentPath, isSidebarOpen, onNavigate, level = 0 }: any) => {
|
|
51
|
-
const isActive = currentPath === item.path;
|
|
52
|
-
const hasActiveChild = item.children?.some((child: any) => child.path === currentPath);
|
|
53
|
-
const [isOpen, setIsOpen] = React.useState(hasActiveChild);
|
|
54
|
-
const Icon = getIcon(item.icon);
|
|
55
|
-
|
|
56
|
-
// Auto-expand if child is active
|
|
57
|
-
React.useEffect(() => {
|
|
58
|
-
if (hasActiveChild) setIsOpen(true);
|
|
59
|
-
}, [hasActiveChild]);
|
|
60
|
-
|
|
61
|
-
if (item.children && item.children.length > 0) {
|
|
62
|
-
return (
|
|
63
|
-
<Collapsible open={isOpen} onOpenChange={setIsOpen} className="w-full">
|
|
64
|
-
<CollapsibleTrigger className={`flex w-full items-center justify-between py-2 text-sm font-medium rounded-md transition-colors text-muted-foreground hover:bg-muted hover:text-foreground ${isSidebarOpen ? 'px-3' : 'justify-center px-2 cursor-pointer'}`}>
|
|
65
|
-
<div className="flex items-center overflow-hidden">
|
|
66
|
-
{Icon && <Icon className={`h-4 w-4 flex-shrink-0 ${isSidebarOpen ? 'mr-3' : ''}`} />}
|
|
67
|
-
<span className={`whitespace-nowrap overflow-hidden transition-all duration-300 ${isSidebarOpen ? 'opacity-100 w-auto' : 'opacity-0 w-0 hidden'}`}>
|
|
68
|
-
{item.label}
|
|
69
|
-
</span>
|
|
70
|
-
</div>
|
|
71
|
-
{isSidebarOpen && (
|
|
72
|
-
<LucideIcons.ChevronDown className={`h-4 w-4 transition-transform ${isOpen ? 'rotate-180' : ''}`} />
|
|
73
|
-
)}
|
|
74
|
-
</CollapsibleTrigger>
|
|
75
|
-
<CollapsibleContent className="space-y-1 overflow-hidden data-[state=closed]:animate-collapsible-up data-[state=open]:animate-collapsible-down">
|
|
76
|
-
{isSidebarOpen && item.children.map((child: any, idx: number) => (
|
|
77
|
-
<NavItem
|
|
78
|
-
key={idx}
|
|
79
|
-
item={child}
|
|
80
|
-
currentPath={currentPath}
|
|
81
|
-
isSidebarOpen={isSidebarOpen}
|
|
82
|
-
onNavigate={onNavigate}
|
|
83
|
-
level={level + 1}
|
|
84
|
-
/>
|
|
85
|
-
))}
|
|
86
|
-
</CollapsibleContent>
|
|
87
|
-
</Collapsible>
|
|
88
|
-
);
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
return (
|
|
92
|
-
<a
|
|
93
|
-
href={item.path || '#'}
|
|
94
|
-
onClick={(e) => item.path && onNavigate(e, item.path)}
|
|
95
|
-
title={!isSidebarOpen ? item.label : undefined}
|
|
96
|
-
className={`flex items-center py-2 text-sm font-medium rounded-md transition-colors ${
|
|
97
|
-
isActive
|
|
98
|
-
? 'bg-primary text-primary-foreground'
|
|
99
|
-
: 'text-muted-foreground hover:bg-muted hover:text-foreground'
|
|
100
|
-
} ${isSidebarOpen ? 'px-3' : 'justify-center px-2'} ${level > 0 && isSidebarOpen ? 'pl-10' : ''}`}
|
|
101
|
-
>
|
|
102
|
-
{Icon && <Icon className={`h-4 w-4 flex-shrink-0 ${isSidebarOpen ? 'mr-3' : ''} ${isActive ? 'text-primary-foreground' : 'text-muted-foreground group-hover:text-foreground'}`} />}
|
|
103
|
-
<span className={`whitespace-nowrap overflow-hidden transition-all duration-300 ${isSidebarOpen ? 'opacity-100 w-auto' : 'opacity-0 w-0 hidden'}`}>
|
|
104
|
-
{item.label}
|
|
105
|
-
</span>
|
|
106
|
-
</a>
|
|
107
|
-
);
|
|
108
|
-
};
|
|
109
|
-
|
|
110
|
-
export const LayoutRenderer = ({ app, children, currentPath, onNavigate }: LayoutRendererProps) => {
|
|
111
|
-
const layout = app.layout || 'sidebar';
|
|
112
|
-
const [isSidbarOpen, setSidebarOpen] = React.useState(true);
|
|
113
|
-
|
|
114
|
-
// Theme management
|
|
115
|
-
const [theme, setTheme] = React.useState<"light" | "dark">("light");
|
|
116
|
-
|
|
117
|
-
React.useEffect(() => {
|
|
118
|
-
const isDark = localStorage.getItem('theme') === 'dark' ||
|
|
119
|
-
(!('theme' in localStorage) && window.matchMedia('(prefers-color-scheme: dark)').matches);
|
|
120
|
-
setTheme(isDark ? 'dark' : 'light');
|
|
121
|
-
}, []);
|
|
122
|
-
|
|
123
|
-
React.useEffect(() => {
|
|
124
|
-
if (theme === 'dark') {
|
|
125
|
-
document.documentElement.classList.add('dark');
|
|
126
|
-
localStorage.setItem('theme', 'dark');
|
|
127
|
-
} else {
|
|
128
|
-
document.documentElement.classList.remove('dark');
|
|
129
|
-
localStorage.setItem('theme', 'light');
|
|
130
|
-
}
|
|
131
|
-
}, [theme]);
|
|
132
|
-
|
|
133
|
-
const toggleTheme = () => {
|
|
134
|
-
setTheme(prev => prev === 'dark' ? 'light' : 'dark');
|
|
135
|
-
};
|
|
136
|
-
|
|
137
|
-
const handleNavClick = (e: React.MouseEvent<HTMLAnchorElement>, path: string) => {
|
|
138
|
-
e.preventDefault();
|
|
139
|
-
if (onNavigate) {
|
|
140
|
-
onNavigate(path);
|
|
141
|
-
} else {
|
|
142
|
-
window.location.href = path;
|
|
143
|
-
}
|
|
144
|
-
};
|
|
145
|
-
|
|
146
|
-
if (layout === 'empty') {
|
|
147
|
-
return <main className={app.className}>{children}</main>;
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
const LogoIcon = app.logo && !app.logo.includes('/') && !app.logo.includes('.') ? getIcon(app.logo) : null;
|
|
151
|
-
|
|
152
|
-
return (
|
|
153
|
-
<div className={`flex min-h-screen w-full bg-background ${app.className || ''}`}>
|
|
154
|
-
{/* Sidebar - Only if configured */}
|
|
155
|
-
{layout === 'sidebar' && (
|
|
156
|
-
<aside
|
|
157
|
-
className={`
|
|
158
|
-
flex-shrink-0 border-r bg-background hidden md:flex flex-col h-screen sticky top-0 z-30 transition-all duration-300 ease-in-out
|
|
159
|
-
${isSidbarOpen ? 'w-64' : 'w-[70px]'}
|
|
160
|
-
`}
|
|
161
|
-
>
|
|
162
|
-
<div className={`h-14 flex items-center border-b font-semibold text-lg tracking-tight transition-all ${isSidbarOpen ? 'px-6' : 'justify-center px-0'}`}>
|
|
163
|
-
{LogoIcon ? (
|
|
164
|
-
<LogoIcon className="h-6 w-6" />
|
|
165
|
-
) : app.logo ? (
|
|
166
|
-
<img src={app.logo} alt={app.title} className="h-6 w-auto" />
|
|
167
|
-
) : <LucideIcons.Box className="h-6 w-6" />}
|
|
168
|
-
|
|
169
|
-
<span className={`ml-2 whitespace-nowrap overflow-hidden transition-all duration-300 ${isSidbarOpen ? 'opacity-100 w-auto' : 'opacity-0 w-0 hidden'}`}>
|
|
170
|
-
{app.title || app.name || 'Object UI'}
|
|
171
|
-
</span>
|
|
172
|
-
</div>
|
|
173
|
-
<nav className="flex-1 p-2 space-y-1 overflow-y-auto overflow-x-hidden">
|
|
174
|
-
{app.menu?.map((item, index) => (
|
|
175
|
-
<NavItem
|
|
176
|
-
key={index}
|
|
177
|
-
item={item}
|
|
178
|
-
currentPath={currentPath}
|
|
179
|
-
isSidebarOpen={isSidbarOpen}
|
|
180
|
-
onNavigate={handleNavClick}
|
|
181
|
-
/>
|
|
182
|
-
))}
|
|
183
|
-
</nav>
|
|
184
|
-
{app.version && isSidbarOpen && (
|
|
185
|
-
<div className="p-4 border-t text-xs text-muted-foreground">
|
|
186
|
-
v{app.version}
|
|
187
|
-
</div>
|
|
188
|
-
)}
|
|
189
|
-
</aside>
|
|
190
|
-
)}
|
|
191
|
-
|
|
192
|
-
{/* Main Content Area */}
|
|
193
|
-
<div className="flex-1 flex flex-col min-w-0 overflow-hidden">
|
|
194
|
-
{/* Header - Always shown in sidebar/header layouts */}
|
|
195
|
-
<header className="h-14 flex items-center justify-between px-4 md:px-6 bg-background/95 backdrop-blur supports-[backdrop-filter]:bg-background/60 border-b z-20 sticky top-0">
|
|
196
|
-
<div className="flex items-center gap-4">
|
|
197
|
-
{/* Toggle Sidebar Button */}
|
|
198
|
-
<button
|
|
199
|
-
onClick={() => setSidebarOpen(!isSidbarOpen)}
|
|
200
|
-
className="p-2 -ml-2 text-muted-foreground hover:bg-muted hover:text-foreground rounded-md transition-colors"
|
|
201
|
-
>
|
|
202
|
-
<LucideIcons.Menu className="h-5 w-5" />
|
|
203
|
-
</button>
|
|
204
|
-
|
|
205
|
-
{/* Breadcrumbs placeholder or Search */}
|
|
206
|
-
<div className="relative hidden md:block w-96">
|
|
207
|
-
<LucideIcons.Search className="absolute left-2.5 top-2.5 h-4 w-4 text-muted-foreground" />
|
|
208
|
-
<input
|
|
209
|
-
type="text"
|
|
210
|
-
placeholder="Search..."
|
|
211
|
-
className="w-full h-9 pl-9 pr-4 rounded-md border border-input bg-background text-sm shadow-sm transition-colors file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50"
|
|
212
|
-
/>
|
|
213
|
-
</div>
|
|
214
|
-
</div>
|
|
215
|
-
<div className="flex items-center gap-2">
|
|
216
|
-
{/* Theme Toggle */}
|
|
217
|
-
<button
|
|
218
|
-
onClick={toggleTheme}
|
|
219
|
-
className="p-2 text-muted-foreground hover:text-foreground transition-colors rounded-md hover:bg-muted"
|
|
220
|
-
title={`Switch to ${theme === 'dark' ? 'light' : 'dark'} mode`}
|
|
221
|
-
>
|
|
222
|
-
{theme === 'dark' ? <LucideIcons.Sun className="h-5 w-5" /> : <LucideIcons.Moon className="h-5 w-5" />}
|
|
223
|
-
</button>
|
|
224
|
-
|
|
225
|
-
{/* Global Actions */}
|
|
226
|
-
{app.actions?.filter(a => a.type === 'button').map((action, i) => {
|
|
227
|
-
const Icon = action.icon ? getIcon(action.icon) : null;
|
|
228
|
-
return (
|
|
229
|
-
<button
|
|
230
|
-
key={i}
|
|
231
|
-
className={action.variant === 'ghost' ? "relative p-2 text-muted-foreground hover:text-foreground transition-colors hover:bg-muted rounded-md" : "p-2"}
|
|
232
|
-
title={action.label}
|
|
233
|
-
>
|
|
234
|
-
{Icon && <Icon className="h-5 w-5" />}
|
|
235
|
-
{action.label && !action.icon && <span>{action.label}</span>}
|
|
236
|
-
</button>
|
|
237
|
-
);
|
|
238
|
-
})}
|
|
239
|
-
|
|
240
|
-
{/* Fallback Bell if no actions defined, or keep it as specific logic?
|
|
241
|
-
The original code hardcoded a Bell button.
|
|
242
|
-
The app.json defines a 'Bell' button action.
|
|
243
|
-
So I should iterate app.actions for buttons as well.
|
|
244
|
-
*/}
|
|
245
|
-
|
|
246
|
-
{/* Original Bell Logic (Hardcoded in user request? No, it was hardcoded in my previous edit, but app.json has it too)
|
|
247
|
-
Let's check app.json. It has:
|
|
248
|
-
{ "type": "button", "variant": "ghost", "size": "icon", "icon": "Bell" }
|
|
249
|
-
|
|
250
|
-
If I render actions generically, I don't need the hardcoded Bell.
|
|
251
|
-
*/}
|
|
252
|
-
|
|
253
|
-
{(!app.actions || !app.actions.some(a => a.type === 'button')) && (
|
|
254
|
-
<button className="relative p-2 text-muted-foreground hover:text-foreground transition-colors">
|
|
255
|
-
<LucideIcons.Bell className="h-5 w-5" />
|
|
256
|
-
<span className="absolute top-1.5 right-1.5 h-2 w-2 bg-red-600 rounded-full border-2 border-background"></span>
|
|
257
|
-
</button>
|
|
258
|
-
)}
|
|
259
|
-
|
|
260
|
-
{app.actions?.filter(a => a.type === 'user').map((userAction, i) => (
|
|
261
|
-
<DropdownMenu key={i}>
|
|
262
|
-
<DropdownMenuTrigger asChild>
|
|
263
|
-
<button className="relative h-8 w-8 rounded-full border bg-muted overflow-hidden focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 hover:opacity-90 transition-opacity">
|
|
264
|
-
<Avatar className="h-full w-full">
|
|
265
|
-
<AvatarImage
|
|
266
|
-
src={userAction.avatar}
|
|
267
|
-
alt={userAction.label || 'User'}
|
|
268
|
-
/>
|
|
269
|
-
<AvatarFallback>
|
|
270
|
-
{userAction.label?.substring(0, 2).toUpperCase() || 'JD'}
|
|
271
|
-
</AvatarFallback>
|
|
272
|
-
</Avatar>
|
|
273
|
-
</button>
|
|
274
|
-
</DropdownMenuTrigger>
|
|
275
|
-
<DropdownMenuContent className="w-56" align="end" forceMount>
|
|
276
|
-
<DropdownMenuLabel className="font-normal">
|
|
277
|
-
<div className="flex flex-col space-y-1">
|
|
278
|
-
<p className="text-sm font-medium leading-none">{userAction.label || 'User'}</p>
|
|
279
|
-
<p className="text-xs leading-none text-muted-foreground">
|
|
280
|
-
{userAction.description || 'user@example.com'}
|
|
281
|
-
</p>
|
|
282
|
-
</div>
|
|
283
|
-
</DropdownMenuLabel>
|
|
284
|
-
<DropdownMenuSeparator />
|
|
285
|
-
<DropdownMenuGroup>
|
|
286
|
-
{userAction.items?.map((item, idx) => {
|
|
287
|
-
if (item.type === 'separator') {
|
|
288
|
-
return <DropdownMenuSeparator key={idx} />;
|
|
289
|
-
}
|
|
290
|
-
return (
|
|
291
|
-
<DropdownMenuItem key={idx} onSelect={() => {
|
|
292
|
-
if ((item as any).onClick) {
|
|
293
|
-
// Handle click logic
|
|
294
|
-
console.log('Clicked', item.label);
|
|
295
|
-
}
|
|
296
|
-
}}>
|
|
297
|
-
{item.label}
|
|
298
|
-
{(item as any).shortcut && (
|
|
299
|
-
<DropdownMenuShortcut>{(item as any).shortcut}</DropdownMenuShortcut>
|
|
300
|
-
)}
|
|
301
|
-
</DropdownMenuItem>
|
|
302
|
-
);
|
|
303
|
-
})}
|
|
304
|
-
</DropdownMenuGroup>
|
|
305
|
-
</DropdownMenuContent>
|
|
306
|
-
</DropdownMenu>
|
|
307
|
-
))}
|
|
308
|
-
</div>
|
|
309
|
-
</header>
|
|
310
|
-
|
|
311
|
-
{/* Page Content */}
|
|
312
|
-
<main className="flex-1 overflow-auto p-4 md:p-8 scroll-smooth">
|
|
313
|
-
<div className="mx-auto max-w-7xl animate-in fade-in slide-in-from-bottom-4 duration-500">
|
|
314
|
-
{children}
|
|
315
|
-
</div>
|
|
316
|
-
</main>
|
|
317
|
-
</div>
|
|
318
|
-
</div>
|
|
319
|
-
);
|
|
320
|
-
};
|
package/src/index.css
DELETED
|
@@ -1,111 +0,0 @@
|
|
|
1
|
-
@import 'tailwindcss';
|
|
2
|
-
|
|
3
|
-
/* Scan sources for Tailwind classes */
|
|
4
|
-
@source './src/**/*.{ts,tsx}';
|
|
5
|
-
@source '../../packages/components/src/**/*.{ts,tsx}';
|
|
6
|
-
@source '../../packages/react/src/**/*.{ts,tsx}';
|
|
7
|
-
@source '../../packages/plugin-kanban/src/**/*.{ts,tsx}';
|
|
8
|
-
@source '../../packages/plugin-charts/src/**/*.{ts,tsx}';
|
|
9
|
-
|
|
10
|
-
/* Tailwind plugin for animations */
|
|
11
|
-
@plugin 'tailwindcss-animate';
|
|
12
|
-
|
|
13
|
-
/* Define theme colors for Tailwind 4 */
|
|
14
|
-
@theme {
|
|
15
|
-
/* Border radius tokens */
|
|
16
|
-
--radius-lg: var(--radius);
|
|
17
|
-
--radius-md: calc(var(--radius) - 2px);
|
|
18
|
-
--radius-sm: calc(var(--radius) - 4px);
|
|
19
|
-
|
|
20
|
-
/* Color tokens mapped to CSS variables */
|
|
21
|
-
--color-border: hsl(var(--border));
|
|
22
|
-
--color-input: hsl(var(--input));
|
|
23
|
-
--color-ring: hsl(var(--ring));
|
|
24
|
-
--color-background: hsl(var(--background));
|
|
25
|
-
--color-foreground: hsl(var(--foreground));
|
|
26
|
-
--color-primary: hsl(var(--primary));
|
|
27
|
-
--color-primary-foreground: hsl(var(--primary-foreground));
|
|
28
|
-
--color-secondary: hsl(var(--secondary));
|
|
29
|
-
--color-secondary-foreground: hsl(var(--secondary-foreground));
|
|
30
|
-
--color-destructive: hsl(var(--destructive));
|
|
31
|
-
--color-destructive-foreground: hsl(var(--destructive-foreground));
|
|
32
|
-
--color-muted: hsl(var(--muted));
|
|
33
|
-
--color-muted-foreground: hsl(var(--muted-foreground));
|
|
34
|
-
--color-accent: hsl(var(--accent));
|
|
35
|
-
--color-accent-foreground: hsl(var(--accent-foreground));
|
|
36
|
-
--color-popover: hsl(var(--popover));
|
|
37
|
-
--color-popover-foreground: hsl(var(--popover-foreground));
|
|
38
|
-
--color-card: hsl(var(--card));
|
|
39
|
-
--color-card-foreground: hsl(var(--card-foreground));
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
:root {
|
|
43
|
-
--background: 0 0% 100%;
|
|
44
|
-
--foreground: 222.2 84% 4.9%;
|
|
45
|
-
|
|
46
|
-
--card: 0 0% 100%;
|
|
47
|
-
--card-foreground: 222.2 84% 4.9%;
|
|
48
|
-
|
|
49
|
-
--popover: 0 0% 100%;
|
|
50
|
-
--popover-foreground: 222.2 84% 4.9%;
|
|
51
|
-
|
|
52
|
-
--primary: 222.2 47.4% 11.2%;
|
|
53
|
-
--primary-foreground: 210 40% 98%;
|
|
54
|
-
|
|
55
|
-
--secondary: 210 40% 96.1%;
|
|
56
|
-
--secondary-foreground: 222.2 47.4% 11.2%;
|
|
57
|
-
|
|
58
|
-
--muted: 210 40% 96.1%;
|
|
59
|
-
--muted-foreground: 215.4 16.3% 46.9%;
|
|
60
|
-
|
|
61
|
-
--accent: 210 40% 96.1%;
|
|
62
|
-
--accent-foreground: 222.2 47.4% 11.2%;
|
|
63
|
-
|
|
64
|
-
--destructive: 0 84.2% 60.2%;
|
|
65
|
-
--destructive-foreground: 210 40% 98%;
|
|
66
|
-
|
|
67
|
-
--border: 214.3 31.8% 91.4%;
|
|
68
|
-
--input: 214.3 31.8% 91.4%;
|
|
69
|
-
--ring: 222.2 84% 4.9%;
|
|
70
|
-
|
|
71
|
-
--radius: 0.5rem;
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
.dark {
|
|
75
|
-
--background: 222.2 84% 4.9%;
|
|
76
|
-
--foreground: 210 40% 98%;
|
|
77
|
-
|
|
78
|
-
--card: 222.2 84% 4.9%;
|
|
79
|
-
--card-foreground: 210 40% 98%;
|
|
80
|
-
|
|
81
|
-
--popover: 222.2 84% 4.9%;
|
|
82
|
-
--popover-foreground: 210 40% 98%;
|
|
83
|
-
|
|
84
|
-
--primary: 210 40% 98%;
|
|
85
|
-
--primary-foreground: 222.2 47.4% 11.2%;
|
|
86
|
-
|
|
87
|
-
--secondary: 217.2 32.6% 17.5%;
|
|
88
|
-
--secondary-foreground: 210 40% 98%;
|
|
89
|
-
|
|
90
|
-
--muted: 217.2 32.6% 17.5%;
|
|
91
|
-
--muted-foreground: 215 20.2% 65.1%;
|
|
92
|
-
|
|
93
|
-
--accent: 217.2 32.6% 17.5%;
|
|
94
|
-
--accent-foreground: 210 40% 98%;
|
|
95
|
-
|
|
96
|
-
--destructive: 0 62.8% 30.6%;
|
|
97
|
-
--destructive-foreground: 210 40% 98%;
|
|
98
|
-
|
|
99
|
-
--border: 217.2 32.6% 17.5%;
|
|
100
|
-
--input: 217.2 32.6% 17.5%;
|
|
101
|
-
--ring: 212.7 26.8% 83.9%;
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
* {
|
|
105
|
-
border-color: hsl(var(--border));
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
body {
|
|
109
|
-
background-color: hsl(var(--background));
|
|
110
|
-
color: hsl(var(--foreground));
|
|
111
|
-
}
|
|
@@ -1,103 +0,0 @@
|
|
|
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 { AppSchema, PageSchema } from '@object-ui/types';
|
|
10
|
-
|
|
11
|
-
export interface MetadataLoader {
|
|
12
|
-
loadAppConfig(): Promise<AppSchema | null>;
|
|
13
|
-
loadPage(path: string): Promise<PageSchema | null>;
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
/**
|
|
17
|
-
* Strategy A: Local Bundle Loader (Vite Glob)
|
|
18
|
-
* Used during local development via 'pnpm dev:crm'
|
|
19
|
-
*/
|
|
20
|
-
export class LocalBundleLoader implements MetadataLoader {
|
|
21
|
-
private appGlob = import.meta.glob('../app-data/app.json');
|
|
22
|
-
private pagesGlob = import.meta.glob('../app-data/pages/**/*.json');
|
|
23
|
-
private rootGlob = import.meta.glob('../app-data/*.json');
|
|
24
|
-
|
|
25
|
-
async loadAppConfig(): Promise<AppSchema | null> {
|
|
26
|
-
const key = '../app-data/app.json';
|
|
27
|
-
if (this.appGlob[key]) {
|
|
28
|
-
const mod: any = await this.appGlob[key]();
|
|
29
|
-
return mod.default || mod;
|
|
30
|
-
}
|
|
31
|
-
return null;
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
async loadPage(path: string): Promise<PageSchema | null> {
|
|
35
|
-
// 1. Normalize Path
|
|
36
|
-
const normalizedPath = path.replace(/^\//, '') || 'index';
|
|
37
|
-
|
|
38
|
-
// 2. Try Exact Match in Pages
|
|
39
|
-
if (await this.tryLoad(`../app-data/pages/${normalizedPath}.json`)) {
|
|
40
|
-
return await this.loadKey(`../app-data/pages/${normalizedPath}.json`);
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
// 3. Try Index Match
|
|
44
|
-
if (await this.tryLoad(`../app-data/pages/${normalizedPath}/index.json`)) {
|
|
45
|
-
return await this.loadKey(`../app-data/pages/${normalizedPath}/index.json`);
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
// 4. Try Root fallback
|
|
49
|
-
if (normalizedPath === 'index' && await this.tryLoad(`../app-data/index.json`)) {
|
|
50
|
-
return await this.loadKey(`../app-data/index.json`);
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
// 5. Dynamic Routing (Basic Mock)
|
|
54
|
-
// TODO: Implement proper glob matching for dynamic routes if needed here,
|
|
55
|
-
// but usually exact paths are enough for this loader demo.
|
|
56
|
-
return null;
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
private async tryLoad(key: string) {
|
|
60
|
-
return !!(this.pagesGlob[key] || this.rootGlob[key]);
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
private async loadKey(key: string) {
|
|
64
|
-
const loader = this.pagesGlob[key] || this.rootGlob[key];
|
|
65
|
-
if (!loader) return null;
|
|
66
|
-
const mod: any = await loader();
|
|
67
|
-
return mod.default || mod;
|
|
68
|
-
}
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
/**
|
|
72
|
-
* Strategy B: Network Loader (Fetch API)
|
|
73
|
-
* Used in production to fetch JSONs from an API endpoint
|
|
74
|
-
*/
|
|
75
|
-
export class NetworkLoader implements MetadataLoader {
|
|
76
|
-
private baseUrl: string;
|
|
77
|
-
|
|
78
|
-
constructor(baseUrl: string = '/api') {
|
|
79
|
-
this.baseUrl = baseUrl;
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
async loadAppConfig(): Promise<AppSchema | null> {
|
|
83
|
-
try {
|
|
84
|
-
const res = await fetch(`${this.baseUrl}/app.json`);
|
|
85
|
-
if (!res.ok) return null;
|
|
86
|
-
return await res.json();
|
|
87
|
-
} catch {
|
|
88
|
-
return null;
|
|
89
|
-
}
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
async loadPage(path: string): Promise<PageSchema | null> {
|
|
93
|
-
try {
|
|
94
|
-
// Maps /customers -> /api/pages/customers.json
|
|
95
|
-
const jsonPath = path === '/' ? '/index' : path;
|
|
96
|
-
const res = await fetch(`${this.baseUrl}/pages${jsonPath}.json`);
|
|
97
|
-
if (!res.ok) return null;
|
|
98
|
-
return await res.json();
|
|
99
|
-
} catch {
|
|
100
|
-
return null;
|
|
101
|
-
}
|
|
102
|
-
}
|
|
103
|
-
}
|
|
@@ -1,56 +0,0 @@
|
|
|
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 type { DataSource } from '@object-ui/core';
|
|
10
|
-
|
|
11
|
-
/**
|
|
12
|
-
* 模拟数据源 (Mock Adapter)
|
|
13
|
-
* 在真实项目中,你会在这里使用 fetch/axios 调用你的 API。
|
|
14
|
-
*/
|
|
15
|
-
export class MockDataSource implements DataSource {
|
|
16
|
-
async find(resource: string, params?: any): Promise<any[]> {
|
|
17
|
-
console.log(`[DataSource] Querying ${resource}`, params);
|
|
18
|
-
return [];
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
async findOne(resource: string, id: string): Promise<any> {
|
|
22
|
-
return null;
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
async create(resource: string, data: any): Promise<any> {
|
|
26
|
-
// 模拟网络请求
|
|
27
|
-
await new Promise(resolve => setTimeout(resolve, 800));
|
|
28
|
-
|
|
29
|
-
console.log(`[DataSource] Created ${resource}:`, data);
|
|
30
|
-
alert(`Success! Created record in "${resource}":\n${JSON.stringify(data, null, 2)}`);
|
|
31
|
-
|
|
32
|
-
return { id: Math.random().toString(), ...data };
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
async update(resource: string, id: string, data: any): Promise<any> {
|
|
36
|
-
return data;
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
async delete(resource: string, id: string): Promise<any> {
|
|
40
|
-
return true;
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
async getObjectSchema(objectName: string): Promise<any> {
|
|
44
|
-
if (!objectName || typeof objectName !== 'string') {
|
|
45
|
-
throw new Error('Invalid object name');
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
console.log(`[DataSource] Getting schema for ${objectName}`);
|
|
49
|
-
// Return a minimal schema for mock purposes
|
|
50
|
-
return {
|
|
51
|
-
name: objectName,
|
|
52
|
-
label: objectName,
|
|
53
|
-
fields: {}
|
|
54
|
-
};
|
|
55
|
-
}
|
|
56
|
-
}
|
package/src/main.tsx
DELETED
|
@@ -1,18 +0,0 @@
|
|
|
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 React from 'react'
|
|
10
|
-
import ReactDOM from 'react-dom/client'
|
|
11
|
-
import App from './App.tsx'
|
|
12
|
-
import './index.css'
|
|
13
|
-
|
|
14
|
-
ReactDOM.createRoot(document.getElementById('root')!).render(
|
|
15
|
-
<React.StrictMode>
|
|
16
|
-
<App />
|
|
17
|
-
</React.StrictMode>,
|
|
18
|
-
)
|
|
@@ -1,64 +0,0 @@
|
|
|
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
|
-
import { describe, it, expect } from 'vitest';
|
|
11
|
-
import { kanbanComponents } from '@object-ui/plugin-kanban';
|
|
12
|
-
import { chartComponents } from '@object-ui/plugin-charts';
|
|
13
|
-
import { ComponentRegistry } from '@object-ui/core';
|
|
14
|
-
|
|
15
|
-
describe('Plugin Integration Protocol', () => {
|
|
16
|
-
describe('Kanban Plugin', () => {
|
|
17
|
-
it('should export components object for manual registration', () => {
|
|
18
|
-
expect(kanbanComponents).toBeDefined();
|
|
19
|
-
expect(kanbanComponents.kanban).toBeDefined();
|
|
20
|
-
});
|
|
21
|
-
|
|
22
|
-
it('should contain valid React component', () => {
|
|
23
|
-
const Component = kanbanComponents.kanban;
|
|
24
|
-
expect(typeof Component).toBe('function'); // React components are functions
|
|
25
|
-
});
|
|
26
|
-
});
|
|
27
|
-
|
|
28
|
-
describe('Charts Plugin', () => {
|
|
29
|
-
it('should export component objects for manual registration', () => {
|
|
30
|
-
expect(chartComponents).toBeDefined();
|
|
31
|
-
expect(chartComponents['bar-chart']).toBeDefined();
|
|
32
|
-
expect(chartComponents['chart']).toBeDefined();
|
|
33
|
-
});
|
|
34
|
-
|
|
35
|
-
it('should export correctly named "bar-chart" key matching schema usage', () => {
|
|
36
|
-
// Critical check for the bug we fixed (type mismatch)
|
|
37
|
-
expect(Object.keys(chartComponents)).toContain('bar-chart');
|
|
38
|
-
expect(chartComponents['bar-chart']).toBeDefined();
|
|
39
|
-
});
|
|
40
|
-
});
|
|
41
|
-
|
|
42
|
-
describe('Manual Registration Simulation', () => {
|
|
43
|
-
it('should successfully register kanban via manual components', () => {
|
|
44
|
-
// Clear registry to simulate clean state
|
|
45
|
-
// Note: In a real app we wouldn't clear, but here we want to prove registration works
|
|
46
|
-
|
|
47
|
-
// Act: Manually register
|
|
48
|
-
if (kanbanComponents?.kanban) {
|
|
49
|
-
ComponentRegistry.register('test-kanban-manual', kanbanComponents.kanban);
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
// Assert
|
|
53
|
-
expect(ComponentRegistry.get('test-kanban-manual')).toBeDefined();
|
|
54
|
-
});
|
|
55
|
-
|
|
56
|
-
it('should successfully register bar-chart via manual components', () => {
|
|
57
|
-
if (chartComponents?.['bar-chart']) {
|
|
58
|
-
ComponentRegistry.register('test-bar-chart-manual', chartComponents['bar-chart']);
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
expect(ComponentRegistry.get('test-bar-chart-manual')).toBeDefined();
|
|
62
|
-
});
|
|
63
|
-
});
|
|
64
|
-
});
|