Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 19 additions & 3 deletions ROADMAP.md
Original file line number Diff line number Diff line change
@@ -1,19 +1,19 @@
# ObjectUI Development Roadmap

> **Last Updated:** March 23, 2026
> **Last Updated:** April 1, 2026
> **Current Version:** v0.5.x
> **Spec Version:** @objectstack/spec v3.3.0
> **Client Version:** @objectstack/client v3.3.0
> **Target UX Benchmark:** 🎯 Airtable parity
> **Current Priority:** AppShell Navigation · Designer Interaction · **View Config Live Preview Sync ✅** · Dashboard Config Panel · Airtable UX Polish · **Flow Designer ✅** · **App Creation & Editing Flow ✅** · **System Settings & App Management ✅** · **Right-Side Visual Editor Drawer ✅** · **Object Manager & Field Designer ✅** · **AI SDUI Chatbot (service-ai + vercel/ai) ✅**
> **Current Priority:** AppShell Navigation · Designer Interaction · **View Config Live Preview Sync ✅** · Dashboard Config Panel · Airtable UX Polish · **Flow Designer ✅** · **App Creation & Editing Flow ✅** · **System Settings & App Management ✅** · **Right-Side Visual Editor Drawer ✅** · **Object Manager & Field Designer ✅** · **AI SDUI Chatbot (service-ai + vercel/ai) ✅** · **Unified Home Dashboard ✅**

---

## 📋 Executive Summary

ObjectUI is a universal Server-Driven UI (SDUI) engine built on React + Tailwind + Shadcn. It renders JSON metadata from the @objectstack/spec protocol into pixel-perfect, accessible, and interactive enterprise interfaces.

**Where We Are:** Foundation is **solid and shipping** — 35 packages, 99+ components, 6,700+ tests, 80 Storybook stories, 43/43 builds passing, ~85% protocol alignment. SpecBridge, Expression Engine, Action Engine, data binding, all view plugins (Grid/Kanban/Calendar/Gantt/Timeline/Map/Gallery), Record components, Report engine, Dashboard BI features, mobile UX, i18n (11 locales), WCAG AA accessibility, Console through Phase 20 (L3), **AppShell Navigation Renderer** (P0.1), **Flow Designer** (P2.4), **Feed/Chatter UI** (P1.5), **App Creation & Editing Flow** (P1.11), **System Settings & App Management** (P1.12), **Page/Dashboard Editor Console Integration** (P1.11), **Right-Side Visual Editor Drawer** (P1.11), and **Console Engine Schema Integration** (P1.14) — all ✅ complete. **ViewDesigner** has been removed — its capabilities (drag-to-reorder, undo/redo) are now provided by the ViewConfigPanel (right-side config panel).
**Where We Are:** Foundation is **solid and shipping** — 35 packages, 99+ components, 6,700+ tests, 80 Storybook stories, 43/43 builds passing, ~85% protocol alignment. SpecBridge, Expression Engine, Action Engine, data binding, all view plugins (Grid/Kanban/Calendar/Gantt/Timeline/Map/Gallery), Record components, Report engine, Dashboard BI features, mobile UX, i18n (11 locales), WCAG AA accessibility, Console through Phase 20 (L3), **AppShell Navigation Renderer** (P0.1), **Flow Designer** (P2.4), **Feed/Chatter UI** (P1.5), **App Creation & Editing Flow** (P1.11), **System Settings & App Management** (P1.12), **Page/Dashboard Editor Console Integration** (P1.11), **Right-Side Visual Editor Drawer** (P1.11), **Console Engine Schema Integration** (P1.14), and **Unified Home Dashboard** (P1.7.1) — all ✅ complete. **ViewDesigner** has been removed — its capabilities (drag-to-reorder, undo/redo) are now provided by the ViewConfigPanel (right-side config panel).

**What Remains:** The gap to **Airtable-level UX** is primarily in:
1. ~~**AppShell** — No dynamic navigation renderer from spec JSON (last P0 blocker)~~ ✅ Complete
Expand Down Expand Up @@ -222,6 +222,22 @@ ObjectUI is a universal Server-Driven UI (SDUI) engine built on React + Tailwind
- ✅ `useNavigationOverlay` hook delegates `new_window` to `onNavigate` when available for app-specific URL control
- ✅ plugin-view `handleRowClick` supports `split` and `popover` branches

### P1.7.1 Console — Unified Home Dashboard (Workspace) ✅

- [x] **HomePage component** — Unified landing page displaying all available applications
- [x] **Route integration** — `/home` route added with proper authentication guards
- [x] **App cards grid** — Responsive grid layout showing all active apps with icons, descriptions, and branding colors
- [x] **QuickActions section** — Quick access cards for creating apps, managing objects, and system settings
- [x] **Recent items** — Display recently accessed objects, dashboards, and pages using `useRecentItems` hook
- [x] **Starred items** — Display user-favorited items using `useFavorites` hook with star/unstar toggle
- [x] **Empty state** — Helpful guidance for new users with "Create First App" and "System Settings" CTAs
- [x] **i18n support** — All labels support internationalization via `useObjectTranslation`
Copy link

Copilot AI Apr 1, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This roadmap entry claims full i18n support for the Home dashboard, but the new HomePage still contains several hard-coded English strings (e.g., loading + empty state copy/buttons). Either update the implementation to fully use i18n keys or soften this checklist item so the roadmap stays accurate.

Suggested change
- [x] **i18n support**All labels support internationalization via `useObjectTranslation`
- [x] **i18n wiring**Home dashboard uses `useObjectTranslation` for labels; remaining hard-coded loading/empty-state/button text is tracked as follow-up work

Copilot uses AI. Check for mistakes.
- [x] **RootRedirect update** — Root path (`/`) now redirects to `/home` instead of first app
- [x] **Responsive design** — Mobile-friendly grid layouts that adapt to screen size
- [x] **Airtable/Notion UX pattern** — Inspired by industry-leading workspace home pages

**Impact:** Users now have a unified workspace dashboard that provides overview of all applications, quick actions, and recent activity. This eliminates the previous behavior of auto-redirecting to the first app, giving users better control and visibility.

### P1.8 Console — View Config Panel (Phase 20)

- [x] Inline ViewConfigPanel for all view types (Airtable-style right sidebar)
Expand Down
55 changes: 19 additions & 36 deletions apps/console/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,9 @@ const PermissionManagementPage = lazy(() => import('./pages/system/PermissionMan
const AuditLogPage = lazy(() => import('./pages/system/AuditLogPage').then(m => ({ default: m.AuditLogPage })));
const ProfilePage = lazy(() => import('./pages/system/ProfilePage').then(m => ({ default: m.ProfilePage })));

// Home Page (lazy — landing page)
const HomePage = lazy(() => import('./pages/home/HomePage').then(m => ({ default: m.HomePage })));

import { useParams } from 'react-router-dom';
import { ThemeProvider } from './components/theme-provider';
import { ConsoleToaster } from './components/ConsoleToaster';
Expand Down Expand Up @@ -456,44 +459,14 @@ function findFirstRoute(items: any[]): string {
return '';
}

// Redirect root to default app
// Redirect root to home page
function RootRedirect() {
const { apps, loading, error } = useMetadata();
const navigate = useNavigate();
const activeApps = apps.filter((a: any) => a.active !== false);
const defaultApp = activeApps.find((a: any) => a.isDefault === true) || activeApps[0];

const { loading } = useMetadata();

if (loading) return <LoadingScreen />;
if (defaultApp) {
return <Navigate to={`/apps/${defaultApp.name}`} replace />;
}
return (
<div className="h-screen flex items-center justify-center">
<Empty>
<EmptyTitle>{error ? 'Failed to Load Configuration' : 'No Apps Configured'}</EmptyTitle>
<EmptyDescription>
{error
? 'There was an error loading the configuration. You can still create an app or access System Settings.'
: 'No applications have been registered. Create your first app or configure your system.'}
</EmptyDescription>
<div className="mt-4 flex flex-col sm:flex-row items-center gap-3">
<Button
onClick={() => navigate('/create-app')}
data-testid="create-first-app-btn"
>
Create Your First App
</Button>
<Button
variant="outline"
onClick={() => navigate('/system')}
data-testid="go-to-settings-btn"
>
System Settings
</Button>
</div>
</Empty>
</div>
);

// Always redirect to home page
return <Navigate to="/home" replace />;
}

/**
Expand Down Expand Up @@ -531,6 +504,16 @@ export function App() {
<Route path="/login" element={<LoginPage />} />
<Route path="/register" element={<RegisterPage />} />
<Route path="/forgot-password" element={<ForgotPasswordPage />} />
{/* Home Dashboard — unified workspace landing page */}
<Route path="/home" element={
<AuthGuard fallback={<Navigate to="/login" />} loadingFallback={<LoadingScreen />}>
<ConnectedShell>
<Suspense fallback={<LoadingScreen />}>
<HomePage />
</Suspense>
</ConnectedShell>
</AuthGuard>
} />
Comment on lines +507 to +516
Copy link

Copilot AI Apr 1, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This introduces a new top-level /home route and changes / to redirect there, but there are no accompanying console tests covering the new routing/empty-state behavior. Given the existing Vitest coverage in apps/console, please add tests verifying root redirect -> /home and that HomePage renders expected sections based on metadata/favorites/recent items.

Copilot uses AI. Check for mistakes.
{/* Top-level system routes — accessible without any app */}
<Route path="/system/*" element={
<AuthGuard fallback={<Navigate to="/login" />} loadingFallback={<LoadingScreen />}>
Expand Down
99 changes: 99 additions & 0 deletions apps/console/src/pages/home/AppCard.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
/**
* AppCard
*
* Display card for an application with icon, name, description, and favorite toggle.
*
* @module
*/

import { Star, StarOff } from 'lucide-react';
import { Card, CardContent, Button } from '@object-ui/components';
import { useObjectTranslation } from '@object-ui/i18n';
import { resolveI18nLabel } from '../../utils';
import { useFavorites } from '../../hooks/useFavorites';
import { getIcon } from '../../utils/getIcon';
import { cn } from '@object-ui/components';

interface AppCardProps {
app: any;
onClick: () => void;
isFavorite: boolean;
}

export function AppCard({ app, onClick, isFavorite }: AppCardProps) {
const { t } = useObjectTranslation();
const { toggleFavorite } = useFavorites();

const Icon = getIcon(app.icon);
const label = resolveI18nLabel(app.label, t) || app.name;
const description = resolveI18nLabel(app.description, t);
const primaryColor = app.branding?.primaryColor;

const handleToggleFavorite = (e: React.MouseEvent) => {
e.stopPropagation();
toggleFavorite({
id: `app:${app.name}`,
label,
href: `/apps/${app.name}`,
type: 'object',
Copy link

Copilot AI Apr 1, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

toggleFavorite is storing an app favorite with type: 'object'. This causes the item to be mislabeled (and downstream UIs that render by type will treat an app as an object). Consider extending FavoriteItem['type'] to include 'app' (and updating filters/UI), or store a separate kind/entityType to avoid semantic mismatch.

Suggested change
type: 'object',
type: 'app',

Copilot uses AI. Check for mistakes.
});
};

return (
<Card
className="cursor-pointer hover:shadow-lg transition-all group relative"
onClick={onClick}
data-testid={`app-card-${app.name}`}
>
<CardContent className="p-6">
{/* Favorite Button */}
<Button
variant="ghost"
size="sm"
className="absolute top-2 right-2 opacity-0 group-hover:opacity-100 transition-opacity"
onClick={handleToggleFavorite}
data-testid={`favorite-btn-${app.name}`}
>
{isFavorite ? (
<Star className="h-4 w-4 fill-yellow-400 text-yellow-400" />
) : (
<StarOff className="h-4 w-4" />
)}
</Button>

{/* App Icon */}
<div
className={cn('inline-flex p-3 rounded-lg mb-4', primaryColor ? '' : 'bg-primary/10')}
style={primaryColor ? { backgroundColor: `${primaryColor}20` } : {}}
>
<Icon
className="h-8 w-8"
style={primaryColor ? { color: primaryColor } : {}}
/>
Comment on lines +65 to +72
Copy link

Copilot AI Apr 1, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

backgroundColor: ${primaryColor}20`` assumes primaryColor is a 6-digit hex color. In this repo, tests already use `'#000'` (3-digit), which would produce `'#20'` (invalid CSS), so the branding background won't render reliably. Use a helper that safely applies alpha for 3/6/8-digit hex (or use `rgb()`/`color-mix()`/CSS variables) instead of string concatenation.

Copilot uses AI. Check for mistakes.
</div>

{/* App Info */}
<div>
<h3 className="font-semibold text-lg mb-1">{label}</h3>
{description && (
<p className="text-sm text-muted-foreground line-clamp-2">{description}</p>
)}
{!description && (
<p className="text-sm text-muted-foreground">
{t('home.appCard.noDescription', { defaultValue: 'No description' })}
</p>
)}
</div>

{/* App Badge (if default) */}
{app.isDefault && (
<div className="mt-3">
<span className="inline-flex items-center rounded-full bg-blue-50 dark:bg-blue-950 px-2 py-1 text-xs font-medium text-blue-700 dark:text-blue-300">
{t('home.appCard.default', { defaultValue: 'Default' })}
</span>
</div>
)}
</CardContent>
</Card>
);
}
153 changes: 153 additions & 0 deletions apps/console/src/pages/home/HomePage.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,153 @@
/**
* HomePage
*
* Unified Home Dashboard (Workspace) that displays all available applications,
* quick actions, recent items, and favorites. Inspired by Airtable/Notion home pages.
*
* Features:
* - Display all active applications as cards
* - Quick actions for creating apps, importing data, etc.
* - Recent apps section (from useRecentItems)
* - Starred/Favorite apps section (from useFavorites)
* - Empty state guidance for new users
* - Responsive grid layout
* - i18n support
*
* @module
*/

import { useNavigate } from 'react-router-dom';
import { useMetadata } from '../../context/MetadataProvider';
import { useRecentItems } from '../../hooks/useRecentItems';
import { useFavorites } from '../../hooks/useFavorites';
import { useObjectTranslation } from '@object-ui/i18n';
import { resolveI18nLabel } from '../../utils';
Copy link

Copilot AI Apr 1, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

resolveI18nLabel is imported but never used. With noUnusedLocals: true in apps/console/tsconfig.json, this will fail TypeScript compilation. Remove the import or use it where intended.

Suggested change
import { resolveI18nLabel } from '../../utils';

Copilot uses AI. Check for mistakes.
import { QuickActions } from './QuickActions';
import { AppCard } from './AppCard';
import { RecentApps } from './RecentApps';
import { StarredApps } from './StarredApps';
import { Empty, EmptyTitle, EmptyDescription, Button } from '@object-ui/components';
import { Plus, Settings } from 'lucide-react';

export function HomePage() {
const navigate = useNavigate();
const { t } = useObjectTranslation();
const { apps, loading } = useMetadata();
const { recentItems } = useRecentItems();
const { favorites } = useFavorites();

Comment on lines +35 to +38
Copy link

Copilot AI Apr 1, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

useMetadata() also exposes an error, but this page ignores it and will show the "no apps configured" empty state even if metadata loading failed. That can be misleading (and is a regression from the previous RootRedirect error state). Consider rendering an explicit error empty-state when error is set.

Copilot uses AI. Check for mistakes.
// Filter active apps
const activeApps = apps.filter((a: any) => a.active !== false);

// Get recent apps (only apps, not objects/dashboards)
Copy link

Copilot AI Apr 1, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The comment says "Get recent apps (only apps, not objects/dashboards)", but the filter includes object, dashboard, and page. This is misleading documentation and makes the later naming (recentApps) harder to follow.

Suggested change
// Get recent apps (only apps, not objects/dashboards)
// Get recent workspace items (objects, dashboards, and pages) for the RecentApps section

Copilot uses AI. Check for mistakes.
const recentApps = recentItems
.filter(item => item.type === 'object' || item.type === 'dashboard' || item.type === 'page')
.slice(0, 6);

// Get starred apps
const starredApps = favorites
.filter(item => item.type === 'object' || item.type === 'dashboard' || item.type === 'page')
.slice(0, 8);

if (loading) {
return (
<div className="min-h-screen flex items-center justify-center">
<div className="text-muted-foreground">Loading workspace...</div>
</div>
Comment on lines +52 to +56
Copy link

Copilot AI Apr 1, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Several user-facing strings here are hard-coded (e.g., "Loading workspace...") instead of going through useObjectTranslation. This contradicts the PR's stated i18n support and will leave these strings untranslated.

Copilot uses AI. Check for mistakes.
);
}

// Empty state - no apps configured
if (activeApps.length === 0) {
return (
<div className="min-h-screen flex items-center justify-center p-6">
<Empty>
<EmptyTitle>Welcome to ObjectUI</EmptyTitle>
<EmptyDescription>
Get started by creating your first application or configure your system settings.
</EmptyDescription>
<div className="mt-6 flex flex-col sm:flex-row items-center gap-3">
<Button
onClick={() => navigate('/create-app')}
data-testid="create-first-app-btn"
>
<Plus className="mr-2 h-4 w-4" />
Create Your First App
</Button>
<Button
variant="outline"
onClick={() => navigate('/system')}
data-testid="go-to-settings-btn"
>
<Settings className="mr-2 h-4 w-4" />
System Settings
</Button>
</div>
</Empty>
Comment on lines +63 to +86
Copy link

Copilot AI Apr 1, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The empty state strings/buttons are hard-coded (e.g., "Welcome to ObjectUI", "Create Your First App", "System Settings") instead of using t(...). This conflicts with the claimed i18n support for the Home dashboard and will leave these strings untranslated.

Copilot uses AI. Check for mistakes.
</div>
);
}

return (
<div className="min-h-screen bg-background">
{/* Header */}
<div className="border-b bg-background/95 backdrop-blur supports-[backdrop-filter]:bg-background/60">
<div className="container mx-auto px-6 py-6">
<div className="flex items-center justify-between">
<div>
<h1 className="text-3xl font-bold tracking-tight">
{t('home.title', { defaultValue: 'Home' })}
</h1>
<p className="text-muted-foreground mt-1">
{t('home.subtitle', { defaultValue: 'Your workspace dashboard' })}
</p>
</div>
<div className="flex items-center gap-2">
<Button
variant="outline"
onClick={() => navigate('/system')}
data-testid="home-settings-btn"
>
<Settings className="mr-2 h-4 w-4" />
{t('common.settings', { defaultValue: 'Settings' })}
</Button>
</div>
</div>
</div>
</div>

{/* Main Content */}
<div className="container mx-auto px-6 py-8 space-y-8">
{/* Quick Actions */}
<QuickActions />

{/* Starred/Favorite Apps */}
{starredApps.length > 0 && (
<StarredApps items={starredApps} />
)}

{/* Recent Apps */}
{recentApps.length > 0 && (
<RecentApps items={recentApps} />
)}

{/* All Applications */}
<section>
<h2 className="text-2xl font-semibold tracking-tight mb-4">
{t('home.allApps', { defaultValue: 'All Applications' })}
</h2>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-4">
{activeApps.map((app: any) => (
<AppCard
key={app.name}
app={app}
onClick={() => navigate(`/apps/${app.name}`)}
isFavorite={favorites.some(f => f.id === `app:${app.name}`)}
/>
))}
</div>
</section>
</div>
</div>
);
}
Loading
Loading