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
34 changes: 33 additions & 1 deletion ROADMAP.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# ObjectUI Development Roadmap

> **Last Updated:** February 26, 2026
> **Last Updated:** February 27, 2026
> **Current Version:** v0.5.x
> **Spec Version:** @objectstack/spec v3.0.10
> **Client Version:** @objectstack/client v3.0.10
Expand Down Expand Up @@ -972,6 +972,38 @@ The `FlowDesigner` is a canvas-based flow editor that bridges the gap between th
- [x] 411 ListView + ViewTabBar tests passing (255 plugin-list tests including 9 new toolbar/collapse tests)
- [x] 11 AppSidebar tests passing

### P2.9 Platform UI Navigation & Table Optimization ✅

> Platform-level sidebar, navigation, and grid/table UX improvements (Issue #XX).

**Sidebar Navigation:**
- [x] Pin icons show-on-hover: `SidebarMenuAction` in `NavigationRenderer` now uses `showOnHover={!item.pinned}` — unpinned items show pin icon only on hover; pinned items always show unpin icon. Applied to both `action` and leaf navigation item types.
- [x] Search placeholder contrast: Search icon in AppSidebar improved from `opacity-50` → `opacity-70` for better readability.
- [x] Recent section position: Recent items section moved above Record Favorites in AppSidebar for quicker access to recently visited items.
- [x] Favorites section: Already hides automatically when no pinned items exist (verified).
- [x] Resizable sidebar width: `SidebarRail` enhanced with pointer-event drag-to-resize (min 200px, max 480px). Width persisted to `localStorage`. Click toggles sidebar, double-click resets to default. `useSidebar()` hook now exposes `sidebarWidth` and `setSidebarWidth`.

**Grid/Table Field Inference:**
- [x] Percent field auto-inference: `inferColumnType()` in ObjectGrid now detects fields with names containing `probability`, `percent`, `percentage`, `completion`, `progress`, `rate` and assigns `PercentCellRenderer` with progress bar display.
- [x] ISO datetime fallback: ObjectGrid `inferColumnType()` now detects ISO 8601 datetime strings (`YYYY-MM-DDTHH:MM`) in data values as a catch-all for fields whose names don't match date/datetime patterns.
- [x] Date/datetime human-friendly display: `DateCellRenderer` (relative format) and `DateTimeCellRenderer` (split date/time) already registered in field registry for all grid/table views.
- [x] Currency/status/boolean renderers: Already implemented with proper formatting (currency symbol, Badge colors, checkbox display).

**Header & Breadcrumb i18n:**
- [x] AppHeader breadcrumb labels (`Dashboards`, `Pages`, `Reports`, `System`) now use `t()` translation via `useObjectTranslation`.
- [x] `console.breadcrumb` i18n keys added to all 11 locales (en, zh, ja, ko, de, fr, es, pt, ru, ar).
- [x] `header-bar` renderer: `resolveCrumbLabel()` handles both string and `I18nLabel` objects for breadcrumb labels.
- [x] `breadcrumb` renderer: `resolveItemLabel()` handles both string and `I18nLabel` objects for item labels.
Copy link

Copilot AI Feb 28, 2026

Choose a reason for hiding this comment

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

ROADMAP entry still references a placeholder “Issue #XX”. Since this PR states it fixes #899, update this reference so the roadmap item is traceable (and keep the renderer helper names consistent with what’s actually implemented).

Suggested change
- [x] `breadcrumb` renderer: `resolveItemLabel()` handles both string and `I18nLabel` objects for item labels.
- [x] `breadcrumb` renderer: `resolveItemLabel()` handles both string and `I18nLabel` objects for item labels (see Issue #899).

Copilot uses AI. Check for mistakes.

**Tests:**
- [x] 46 NavigationRenderer tests passing (pin/favorites/search/reorder)
- [x] 75 field cell renderer tests passing (date/datetime/select/boolean/percent)
- [x] 263 ObjectGrid tests passing (inference, rendering, accessibility)
- [x] 28 DataTable tests passing
- [x] 78 Layout tests passing (NavigationRenderer + AppSchemaRenderer)
- [x] 11 AppSidebar tests passing
- [x] 32 i18n tests passing

### P2.5 PWA & Offline (Real Sync)

- [ ] Background sync queue → real server sync (replace simulation)
Expand Down
10 changes: 6 additions & 4 deletions apps/console/src/components/AppHeader.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ import { ConnectionStatus } from './ConnectionStatus';
import { ActivityFeed, type ActivityItem } from './ActivityFeed';
import type { ConnectionState } from '../dataSource';
import { useAdapter } from '../context/AdapterProvider';
import { useObjectTranslation } from '@object-ui/i18n';

/** Convert a slug like "crm_dashboard" or "audit-log" to "Crm Dashboard" / "Audit Log" */
function humanizeSlug(slug: string): string {
Expand All @@ -55,6 +56,7 @@ export function AppHeader({ appName, objects, connectionState, presenceUsers, ac
const params = useParams();
const { isOnline } = useOffline();
const dataSource = useAdapter();
const { t } = useObjectTranslation();

const [apiPresenceUsers, setApiPresenceUsers] = useState<PresenceUser[] | null>(null);
const [apiActivities, setApiActivities] = useState<ActivityItem[] | null>(null);
Expand Down Expand Up @@ -110,22 +112,22 @@ export function AppHeader({ appName, objects, connectionState, presenceUsers, ac
];

if (routeType === 'dashboard') {
breadcrumbItems.push({ label: 'Dashboards', href: `${breadcrumbItems[0].href}` });
breadcrumbItems.push({ label: t('console.breadcrumb.dashboards'), href: `${breadcrumbItems[0].href}` });
if (pathParts[3]) {
breadcrumbItems.push({ label: humanizeSlug(pathParts[3]) });
}
} else if (routeType === 'page') {
breadcrumbItems.push({ label: 'Pages', href: `${breadcrumbItems[0].href}` });
breadcrumbItems.push({ label: t('console.breadcrumb.pages'), href: `${breadcrumbItems[0].href}` });
if (pathParts[3]) {
breadcrumbItems.push({ label: humanizeSlug(pathParts[3]) });
}
} else if (routeType === 'report') {
breadcrumbItems.push({ label: 'Reports', href: `${breadcrumbItems[0].href}` });
breadcrumbItems.push({ label: t('console.breadcrumb.reports'), href: `${breadcrumbItems[0].href}` });
if (pathParts[3]) {
breadcrumbItems.push({ label: humanizeSlug(pathParts[3]) });
}
} else if (routeType === 'system') {
breadcrumbItems.push({ label: 'System' });
breadcrumbItems.push({ label: t('console.breadcrumb.system') });
if (pathParts[3]) {
breadcrumbItems.push({ label: humanizeSlug(pathParts[3]) });
}
Expand Down
56 changes: 28 additions & 28 deletions apps/console/src/components/AppSidebar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -393,7 +393,7 @@ export function AppSidebar({ activeAppName, onAppChange }: { activeAppName: stri
{/* Navigation Search */}
<SidebarGroup className="py-0">
<SidebarGroupContent className="relative">
<Search className="pointer-events-none absolute left-2 top-1/2 size-4 -translate-y-1/2 select-none opacity-50" />
<Search className="pointer-events-none absolute left-2 top-1/2 size-4 -translate-y-1/2 select-none opacity-70" />
<SidebarInput
placeholder="Search navigation..."
value={navSearchQuery}
Expand All @@ -416,68 +416,68 @@ export function AppSidebar({ activeAppName, onAppChange }: { activeAppName: stri
onReorder={handleReorder}
/>

{/* Record Favorites */}
{favorites.length > 0 && (
{/* Recent Items (elevated position for quick access) */}
{recentItems.length > 0 && (
<SidebarGroup>
<SidebarGroupLabel className="flex items-center gap-1.5">
<Star className="h-3.5 w-3.5" />
Favorites
<SidebarGroupLabel
className="flex items-center gap-1.5 cursor-pointer select-none"
onClick={() => setRecentExpanded(prev => !prev)}
>
<ChevronRight className={`h-3 w-3 transition-transform duration-150 ${recentExpanded ? 'rotate-90' : ''}`} />
<Clock className="h-3.5 w-3.5" />
Recent
</SidebarGroupLabel>
Comment on lines +422 to 429
Copy link

Copilot AI Feb 28, 2026

Choose a reason for hiding this comment

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

SidebarGroupLabel renders a div by default, but it’s being used as a clickable control here. That’s not keyboard-accessible and won’t announce expanded/collapsed state to assistive tech. Use SidebarGroupLabel asChild with a button (or CollapsibleTrigger) and add aria-expanded/aria-controls so the Recent section can be toggled via keyboard.

Copilot uses AI. Check for mistakes.
{recentExpanded && (
<SidebarGroupContent>
<SidebarMenu>
{favorites.slice(0, 8).map(item => (
{recentItems.slice(0, 5).map(item => (
<SidebarMenuItem key={item.id}>
<SidebarMenuButton asChild tooltip={item.label}>
<Link to={item.href}>
<span className="text-muted-foreground">
{item.type === 'dashboard' ? '📊' : item.type === 'report' ? '📈' : item.type === 'page' ? '📄' : '📋'}
{item.type === 'dashboard' ? '📊' : item.type === 'report' ? '📈' : '📄'}
</span>
<span className="truncate">{item.label}</span>
</Link>
</SidebarMenuButton>
<SidebarMenuAction
showOnHover
onClick={(e: any) => { e.stopPropagation(); removeFavorite(item.id); }}
aria-label={`Remove ${item.label} from favorites`}
>
<StarOff className="h-3 w-3" />
</SidebarMenuAction>
</SidebarMenuItem>
))}
</SidebarMenu>
</SidebarGroupContent>
)}
</SidebarGroup>
)}

{/* Recent Items (default collapsed) */}
{recentItems.length > 0 && (
{/* Record Favorites */}
{favorites.length > 0 && (
<SidebarGroup>
<SidebarGroupLabel
className="flex items-center gap-1.5 cursor-pointer select-none"
onClick={() => setRecentExpanded(prev => !prev)}
>
<ChevronRight className={`h-3 w-3 transition-transform duration-150 ${recentExpanded ? 'rotate-90' : ''}`} />
<Clock className="h-3.5 w-3.5" />
Recent
<SidebarGroupLabel className="flex items-center gap-1.5">
<Star className="h-3.5 w-3.5" />
Favorites
</SidebarGroupLabel>
{recentExpanded && (
<SidebarGroupContent>
<SidebarMenu>
{recentItems.slice(0, 5).map(item => (
{favorites.slice(0, 8).map(item => (
<SidebarMenuItem key={item.id}>
<SidebarMenuButton asChild tooltip={item.label}>
<Link to={item.href}>
<span className="text-muted-foreground">
{item.type === 'dashboard' ? '📊' : item.type === 'report' ? '📈' : '📄'}
{item.type === 'dashboard' ? '📊' : item.type === 'report' ? '📈' : item.type === 'page' ? '📄' : '📋'}
</span>
<span className="truncate">{item.label}</span>
</Link>
</SidebarMenuButton>
<SidebarMenuAction
showOnHover
onClick={(e: any) => { e.stopPropagation(); removeFavorite(item.id); }}
Copy link

Copilot AI Feb 28, 2026

Choose a reason for hiding this comment

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

Avoid using any for React event types in new/modified handlers; it weakens type-safety in a strict TS codebase. Use React.ChangeEvent<HTMLInputElement> for the input change handler and React.MouseEvent for the action click handler (or infer types from the callback parameter).

Suggested change
onClick={(e: any) => { e.stopPropagation(); removeFavorite(item.id); }}
onClick={(e: React.MouseEvent<HTMLButtonElement>) => { e.stopPropagation(); removeFavorite(item.id); }}

Copilot uses AI. Check for mistakes.
aria-label={`Remove ${item.label} from favorites`}
>
<StarOff className="h-3 w-3" />
</SidebarMenuAction>
</SidebarMenuItem>
))}
</SidebarMenu>
</SidebarGroupContent>
)}
</SidebarGroup>
)}
</>
Expand Down
5 changes: 3 additions & 2 deletions packages/components/src/renderers/data-display/breadcrumb.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import { ComponentRegistry } from '@object-ui/core';
import type { BreadcrumbSchema } from '@object-ui/types';
import { Breadcrumb, BreadcrumbList, BreadcrumbItem, BreadcrumbLink, BreadcrumbPage, BreadcrumbSeparator } from '../../ui/breadcrumb';
import { renderChildren } from '../../lib/utils';
import { resolveI18nLabel } from '@object-ui/react';

ComponentRegistry.register('breadcrumb',
({ schema, ...props }: { schema: BreadcrumbSchema; [key: string]: any }) => {
Expand All @@ -31,9 +32,9 @@ ComponentRegistry.register('breadcrumb',
<div key={idx} className="flex items-center">
<BreadcrumbItem>
{idx === (schema.items?.length || 0) - 1 ? (
<BreadcrumbPage>{item.label}</BreadcrumbPage>
<BreadcrumbPage>{resolveI18nLabel(item.label) ?? ''}</BreadcrumbPage>
) : (
<BreadcrumbLink href={item.href}>{item.label}</BreadcrumbLink>
<BreadcrumbLink href={item.href}>{resolveI18nLabel(item.label) ?? ''}</BreadcrumbLink>
)}
</BreadcrumbItem>
{idx < (schema.items?.length || 0) - 1 && <BreadcrumbSeparator />}
Expand Down
5 changes: 3 additions & 2 deletions packages/components/src/renderers/navigation/header-bar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
import React from 'react';
import { ComponentRegistry } from '@object-ui/core';
import type { HeaderBarSchema } from '@object-ui/types';
import { resolveI18nLabel } from '@object-ui/react';
import {
SidebarTrigger,
Separator,
Expand All @@ -31,9 +32,9 @@ ComponentRegistry.register('header-bar',
<React.Fragment key={idx}>
<BreadcrumbItem>
{idx === schema.crumbs.length - 1 ? (
<BreadcrumbPage>{crumb.label}</BreadcrumbPage>
<BreadcrumbPage>{resolveI18nLabel(crumb.label) ?? ''}</BreadcrumbPage>
) : (
<BreadcrumbLink href={crumb.href || '#'}>{crumb.label}</BreadcrumbLink>
<BreadcrumbLink href={crumb.href || '#'}>{resolveI18nLabel(crumb.label) ?? ''}</BreadcrumbLink>
)}
</BreadcrumbItem>
{idx < schema.crumbs.length - 1 && <BreadcrumbSeparator />}
Expand Down
86 changes: 80 additions & 6 deletions packages/components/src/ui/sidebar.tsx

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 6 additions & 0 deletions packages/i18n/src/locales/ar.ts
Original file line number Diff line number Diff line change
Expand Up @@ -273,6 +273,12 @@ const ar = {
console: {
title: 'وحدة تحكم ObjectStack',
initializing: 'جاري تهيئة التطبيق...',
breadcrumb: {
dashboards: 'لوحات المعلومات',
pages: 'الصفحات',
reports: 'التقارير',
system: 'النظام',
},
loadingSteps: {
connecting: 'جاري الاتصال بمصدر البيانات',
loadingConfig: 'جاري تحميل الإعدادات',
Expand Down
6 changes: 6 additions & 0 deletions packages/i18n/src/locales/de.ts
Original file line number Diff line number Diff line change
Expand Up @@ -272,6 +272,12 @@ const de = {
console: {
title: 'ObjectStack Konsole',
initializing: 'Anwendung wird initialisiert...',
breadcrumb: {
dashboards: 'Dashboards',
pages: 'Seiten',
reports: 'Berichte',
system: 'System',
},
loadingSteps: {
connecting: 'Verbindung zur Datenquelle herstellen',
loadingConfig: 'Konfiguration laden',
Expand Down
Loading