Skip to content

Commit ce45b8d

Browse files
committed
Add placeholder renderers for protocol components
Introduces a PlaceholderRenderer and a registerPlaceholders function to provide visual placeholders for protocol-defined components (e.g., view:grid, action:button, widget:metric) in the component registry. Updates the CRM app example to use these placeholders for unimplemented components, improving developer experience and schema prototyping.
1 parent 38e0c3e commit ce45b8d

3 files changed

Lines changed: 128 additions & 4 deletions

File tree

examples/crm-app/src/App.tsx

Lines changed: 19 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,13 @@ import { SchemaRendererProvider, SchemaRenderer, useRenderer } from '@object-ui/
55
import { registerFields } from '@object-ui/fields';
66
import { registerLayout } from '@object-ui/layout';
77
import '@object-ui/plugin-dashboard'; // Auto-register dashboard
8+
import { registerPlaceholders } from '@object-ui/components';
89
import { SidebarNav } from './components/SidebarNav';
910

1011
// 1. Register components from packages (The "Controls Repository")
1112
registerFields();
1213
registerLayout();
14+
registerPlaceholders(); // Register missing components as placeholders
1315

1416
// 2. Define Mock Data (In a real app, this comes from an API)
1517
const mockData = {
@@ -56,9 +58,13 @@ const dashboardSchema = {
5658
{
5759
id: "w4",
5860
layout: { w: 3, h: 2 },
59-
title: "Recent Activity",
61+
title: "Recent Activity (Kanban Placeholder)",
6062
component: {
61-
type: "text", props: { value: "Activity list would go here...", className: "text-gray-500" }
63+
type: "view:kanban",
64+
props: {
65+
columns: ["Todo", "In Progress", "Done"],
66+
data: [{id: 1, title: "Task 1", status: "Todo"}]
67+
}
6268
}
6369
}
6470
]
@@ -77,7 +83,7 @@ const contactsSchema = {
7783
},
7884
children: [
7985
{
80-
type: "button",
86+
type: "action:button",
8187
props: { label: "Add Contact", variant: "default" },
8288
events: { onClick: [{ action: "navigate", params: { url: "/contacts/new" } }] }
8389
}
@@ -88,7 +94,7 @@ const contactsSchema = {
8894
className: "mt-6",
8995
children: [
9096
{
91-
type: "table", // Note: We need to implement 'table' in plugins soon
97+
type: "view:grid",
9298
bind: "contacts",
9399
props: {
94100
columns: [
@@ -99,6 +105,15 @@ const contactsSchema = {
99105
}
100106
}
101107
]
108+
},
109+
{
110+
type: "view:map",
111+
props: {
112+
lat: 34.05,
113+
lng: -118.25,
114+
zoom: 10
115+
},
116+
className: "mt-4 h-64"
102117
}
103118
]
104119
};

packages/components/src/index.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,9 @@ import './renderers';
1515
export { cn } from './lib/utils';
1616
export { renderChildren } from './lib/utils';
1717

18+
// Export placeholder registration
19+
export { registerPlaceholders } from './renderers/placeholders';
20+
1821
// Export raw Shadcn UI components
1922
export * from './ui';
2023

Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
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 { ComponentRegistry } from '@object-ui/core';
11+
import { cn } from '../lib/utils';
12+
import { Box, FileQuestion } from 'lucide-react';
13+
14+
export const PlaceholderRenderer = ({ schema, className, ...props }: any) => {
15+
const type = schema.type;
16+
const isView = type.startsWith('view:');
17+
const isWidget = type.startsWith('widget:');
18+
const isField = type.startsWith('field:');
19+
20+
return (
21+
<div className={cn(
22+
"flex flex-col items-center justify-center p-6 border-2 border-dashed rounded-lg bg-muted/30 transition-colors hover:bg-muted/50",
23+
isView && "border-blue-300 bg-blue-50/50 min-h-[200px]",
24+
isWidget && "border-purple-300 bg-purple-50/50 min-h-[150px]",
25+
isField && "border-yellow-300 bg-yellow-50/50 p-2 min-h-[40px] flex-row gap-2 justify-start",
26+
className
27+
)}>
28+
<div className={cn("flex items-center gap-2 text-muted-foreground", isField && "text-sm")}>
29+
{isField ? <Box className="h-4 w-4" /> : <FileQuestion className="h-8 w-8 mb-2 opacity-50" />}
30+
<div className="flex flex-col items-center text-center">
31+
<span className="font-mono font-medium text-foreground">{type}</span>
32+
{!isField && <span className="text-xs">Component Placeholder</span>}
33+
</div>
34+
</div>
35+
{schema.props && !isField && (
36+
<div className="mt-4 w-full text-xs text-muted-foreground bg-background/50 p-2 rounded overflow-hidden">
37+
<div className="opacity-70">Properties:</div>
38+
<pre className="mt-1 truncate">{JSON.stringify(schema.props, null, 0)}</pre>
39+
</div>
40+
)}
41+
</div>
42+
);
43+
};
44+
45+
// List of all protocol-defined components that need placeholders
46+
const PROTOCOL_COMPONENTS = [
47+
// 1. Views (List)
48+
'view:grid', 'view:kanban', 'view:map', 'view:calendar', 'view:gantt',
49+
'view:timeline', 'view:gallery', 'view:spreadsheet',
50+
51+
// 2. Views (Form)
52+
'view:simple', 'view:wizard', 'view:tabbed', 'view:drawer', 'view:modal', 'view:split',
53+
54+
// 3. Fields (Textual)
55+
'field:text', 'field:textarea', 'field:password', 'field:email', 'field:url', 'field:phone',
56+
57+
// 4. Fields (Rich)
58+
'field:markdown', 'field:html', 'field:code',
59+
60+
// 5. Fields (Numeric)
61+
'field:number', 'field:currency', 'field:percent', 'field:slider',
62+
63+
// 6. Fields (Selection)
64+
'field:boolean', 'field:checkboxes', 'field:select', 'field:multiselect', 'field:radio',
65+
66+
// 7. Fields (Date/Time)
67+
'field:date', 'field:datetime', 'field:time', 'field:duration',
68+
69+
// 8. Fields (Relational)
70+
'field:lookup', 'field:master_detail', 'field:tree',
71+
72+
// 9. Fields (Media)
73+
'field:image', 'field:file', 'field:video', 'field:audio', 'field:avatar',
74+
75+
// 10. Fields (Visual)
76+
'field:color', 'field:rating', 'field:signature', 'field:qrcode', 'field:progress',
77+
78+
// 11. Fields (Structure)
79+
'field:json', 'field:address', 'field:location',
80+
81+
// 12. Page Components
82+
'page:footer', 'page:tabs', 'page:accordion', 'page:card', 'page:sidebar',
83+
'record:details', 'record:highlights', 'record:related_list', 'record:activity',
84+
'record:chatter', 'record:path',
85+
'app:launcher', 'nav:menu', 'nav:breadcrumb',
86+
'global:search', 'global:notifications', 'user:profile',
87+
88+
// 13. Dashboard Widgets
89+
'widget:metric', 'widget:bar', 'widget:line', 'widget:pie', 'widget:funnel',
90+
'widget:radar', 'widget:scatter', 'widget:heatmap', 'widget:pivot', 'widget:table', 'widget:text', 'widget:image',
91+
92+
// 14. Smart Actions
93+
'action:button', 'action:group', 'action:menu', 'action:icon',
94+
95+
// 15. AI
96+
'ai:chat_window', 'ai:input', 'ai:suggestion', 'ai:feedback'
97+
];
98+
99+
export function registerPlaceholders() {
100+
PROTOCOL_COMPONENTS.forEach(type => {
101+
// Only register if not already registered (to avoid overwriting real implementations)
102+
if (!ComponentRegistry.get(type)) {
103+
ComponentRegistry.register(type, PlaceholderRenderer);
104+
}
105+
});
106+
}

0 commit comments

Comments
 (0)