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
39 changes: 39 additions & 0 deletions ROADMAP.md
Original file line number Diff line number Diff line change
Expand Up @@ -781,6 +781,45 @@ The `FlowDesigner` is a canvas-based flow editor that bridges the gap between th
- [x] `exportOptions` schema reconciliation: Zod validator updated to accept both spec `string[]` format and ObjectUI object format via `z.union()`. ListView normalizes string[] to `{ formats }` at render time.
- [x] `pagination.pageSizeOptions` backend integration: Page size selector is now a controlled component that dynamically updates `effectivePageSize`, triggering data re-fetch. `onPageSizeChange` callback fires on selection. Full test coverage for selector rendering, option enumeration, and data reload.

### P2.7 Platform UI Consistency & Interaction Optimization ✅

> All items from the UI consistency optimization (Issue #749) have been implemented.

**Global Theme & Design Tokens:**
- [x] Hardcoded gray colors in `GridField.tsx`, `ReportRenderer.tsx`, and `ObjectGrid.tsx` replaced with theme tokens (`text-muted-foreground`, `bg-muted`, `border-border`, `border-foreground`)
- [x] Global font-family (`Inter`, ui-sans-serif, system-ui) injected in `index.css` `:root`
- [x] `--config-panel-width: 280px` CSS custom property added for unified config panel sizing
- [x] Border radius standardized to `rounded-lg` across report/grid components
- [x] `transition-colors duration-150` added to all interactive elements (toolbar buttons, tab bar, sidebar menu buttons)
- [x] `LayoutRenderer.tsx` outer shell `bg-slate-50/50 dark:bg-zinc-950` replaced with `bg-background` theme token

**Sidebar Navigation:**
- [x] `SidebarMenuButton` active state enhanced with 3px left indicator bar via `before:` pseudo-element
- [x] `SidebarMenuButton` transition expanded to include `color, background-color` with `duration-150`
- [x] `SidebarGroupLabel` visual separator added (`border-t border-border/30 pt-3 mt-2`)
- [x] Collapsed-mode tooltip support in `SidebarNav` via `tooltip={item.title}` prop
- [ ] `LayoutRenderer.tsx` hand-written sidebar → `SidebarNav` unification (deferred: requires extending SidebarNav to support nested menus, logo, version footer)

**ListView Toolbar:**
- [x] Search changed from expandable button to always-visible inline `<Input>` (`w-48`)
- [x] Activated state (`bg-primary/10 border border-primary/20`) added to Filter/Sort/Group/Color buttons when active
- [x] Toolbar overflow improved with `overflow-x-auto` for responsive behavior
- [x] `transition-colors duration-150` added to all toolbar buttons

**ObjectGrid Cell Renderers:**
- [x] `formatRelativeDate()` function added for relative time display ("Today", "2 days ago", "Yesterday")
- [x] DataTable/VirtualGrid header styling unified: `text-[11px] font-semibold uppercase tracking-wider text-muted-foreground/70 bg-muted/30`
- [x] Remaining hardcoded gray colors in ObjectGrid loading spinner and status badge fallback replaced with theme tokens

**ConfigPanelRenderer:**
- [x] `<Separator>` added between sections for visual clarity
- [x] Panel width uses `--config-panel-width` CSS custom property

**View Tab Bar:**
- [x] Tab spacing tightened (`gap-0.5`, `px-3 py-1.5`)
- [x] Active tab indicator changed to bottom border (`border-b-2 border-primary font-medium text-foreground`)
- [x] `transition-colors duration-150` added to tab buttons

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

- [ ] Background sync queue → real server sync (replace simulation)
Expand Down
6 changes: 4 additions & 2 deletions packages/components/src/custom/config-panel-renderer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import { X, Save, RotateCcw, ChevronRight } from 'lucide-react';

import { cn } from '../lib/utils';
import { Button } from '../ui/button';
import { Separator } from '../ui/separator';
import { SectionHeader } from './section-header';
import { ConfigFieldRenderer } from './config-field-renderer';
import type { ConfigPanelSchema } from '../types/config-panel';
Expand Down Expand Up @@ -128,7 +129,7 @@ export function ConfigPanelRenderer({
aria-label={ariaLabel}
tabIndex={tabIndex}
className={cn(
'absolute inset-y-0 right-0 w-full sm:w-72 lg:w-80 sm:relative border-l bg-background flex flex-col shrink-0 z-20',
'absolute inset-y-0 right-0 w-full sm:w-[var(--config-panel-width,280px)] sm:relative border-l bg-background flex flex-col shrink-0 z-20',
className,
)}
>
Expand Down Expand Up @@ -167,13 +168,14 @@ export function ConfigPanelRenderer({

{/* ── Scrollable sections ────────────────────────────── */}
<div className="flex-1 overflow-auto px-4 pb-4">
{schema.sections.map((section) => {
{schema.sections.map((section, sectionIdx) => {
if (section.visibleWhen && !section.visibleWhen(draft)) return null;

const sectionCollapsed = isCollapsed(section.key, section.defaultCollapsed);

return (
<div key={section.key} data-testid={`config-section-${section.key}`}>
{sectionIdx > 0 && <Separator className="my-1" />}
<SectionHeader
Comment on lines +171 to 179
Copy link

Copilot AI Feb 24, 2026

Choose a reason for hiding this comment

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

Section separators are keyed off the original section index (sectionIdx). If earlier sections are hidden via visibleWhen, the first visible section can incorrectly render with a leading . Track the index of the last rendered/visible section (or filter visible sections first) before inserting separators.

Copilot uses AI. Check for mistakes.
title={section.title}
collapsible={section.collapsible}
Expand Down
4 changes: 4 additions & 0 deletions packages/components/src/index.css
Original file line number Diff line number Diff line change
Expand Up @@ -74,11 +74,15 @@

--radius: 0.5rem;

--config-panel-width: 280px;

--chart-1: 12 76% 61%;
--chart-2: 173 58% 39%;
--chart-3: 197 37% 24%;
--chart-4: 43 74% 66%;
--chart-5: 27 87% 67%;

font-family: 'Inter', ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
}

.dark {
Expand Down
12 changes: 6 additions & 6 deletions packages/components/src/renderers/complex/data-table.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -624,18 +624,18 @@ const DataTableRenderer = ({ schema }: { schema: DataTableSchema }) => {
<div className="rounded-md border flex-1 min-h-0 overflow-auto relative bg-background [-webkit-overflow-scrolling:touch] shadow-[inset_-8px_0_8px_-8px_rgba(0,0,0,0.08)]">
<Table>
{caption && <TableCaption>{caption}</TableCaption>}
<TableHeader className="sticky top-0 bg-background z-10 shadow-sm">
<TableHeader className="sticky top-0 bg-muted/30 z-10">
<TableRow>
{selectable && (
<TableHead className={cn("w-12 bg-background", frozenColumns > 0 && "sticky left-0 z-20")}>
<TableHead className={cn("w-12 bg-muted/30", frozenColumns > 0 && "sticky left-0 z-20")}>
<Checkbox
checked={allPageRowsSelected ? true : somePageRowsSelected ? 'indeterminate' : false}
onCheckedChange={handleSelectAll}
/>
</TableHead>
)}
{showRowNumbers && (
<TableHead className={cn("w-12 bg-background text-center", frozenColumns > 0 && "sticky z-20")} style={frozenColumns > 0 ? { left: selectable ? 48 : 0 } : undefined}>
<TableHead className={cn("w-12 bg-muted/30 text-center", frozenColumns > 0 && "sticky z-20")} style={frozenColumns > 0 ? { left: selectable ? 48 : 0 } : undefined}>
<span className="text-xs text-muted-foreground">#</span>
</TableHead>
)}
Expand Down Expand Up @@ -664,7 +664,7 @@ const DataTableRenderer = ({ schema }: { schema: DataTableSchema }) => {
isDragOver && 'border-l-2 border-primary',
col.align === 'right' && 'text-right',
col.align === 'center' && 'text-center',
'relative group bg-background',
'relative group bg-muted/30',
isFrozen && 'sticky z-20',
isFrozen && index === frozenColumns - 1 && 'border-r-2 border-border shadow-[2px_0_4px_-2px_rgba(0,0,0,0.1)]',
)}
Expand Down Expand Up @@ -692,7 +692,7 @@ const DataTableRenderer = ({ schema }: { schema: DataTableSchema }) => {
{col.headerIcon && (
<span className="text-muted-foreground flex-shrink-0">{col.headerIcon}</span>
)}
<span>{col.header}</span>
<span className="text-[11px] font-semibold uppercase tracking-wider text-muted-foreground/70">{col.header}</span>
{sortable && col.sortable !== false && getSortIcon(col.accessorKey)}
</div>
{resizableColumns && col.resizable !== false && (
Expand All @@ -707,7 +707,7 @@ const DataTableRenderer = ({ schema }: { schema: DataTableSchema }) => {
);
})}
{rowActions && (
<TableHead className="w-24 text-right bg-background">Actions</TableHead>
<TableHead className="w-24 text-right bg-muted/30">Actions</TableHead>
)}
</TableRow>
</TableHeader>
Expand Down
14 changes: 12 additions & 2 deletions packages/components/src/ui/sidebar.tsx

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

30 changes: 30 additions & 0 deletions packages/fields/src/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,32 @@ export function formatPercent(value: number, precision: number = 0): string {
return `${displayValue.toFixed(precision)}%`;
}

/**
* Format date as relative time (e.g., "2 days ago", "Today", "Overdue 3d")
Copy link

Copilot AI Feb 24, 2026

Choose a reason for hiding this comment

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

The doc comment mentions an "Overdue 3d" output, but formatRelativeDate() never returns an "Overdue"-prefixed string. Either adjust the comment to match current behavior or implement the overdue formatting described.

Suggested change
* Format date as relative time (e.g., "2 days ago", "Today", "Overdue 3d")
* Format date as relative time (e.g., "2 days ago", "Today", "In 3 days")

Copilot uses AI. Check for mistakes.
*/
export function formatRelativeDate(value: string | Date): string {
if (!value) return '-';
const date = typeof value === 'string' ? new Date(value) : value;
if (isNaN(date.getTime())) return '-';

const now = new Date();
const startOfToday = new Date(now.getFullYear(), now.getMonth(), now.getDate());
const startOfDate = new Date(date.getFullYear(), date.getMonth(), date.getDate());
const diffMs = startOfDate.getTime() - startOfToday.getTime();
const diffDays = Math.round(diffMs / (1000 * 60 * 60 * 24));

if (diffDays === 0) return 'Today';
if (diffDays === 1) return 'Tomorrow';
if (diffDays === -1) return 'Yesterday';
if (diffDays < -1) {
const absDays = Math.abs(diffDays);
if (absDays <= 30) return `${absDays} days ago`;
return formatDate(date);
}
if (diffDays > 1 && diffDays <= 30) return `In ${diffDays} days`;
return formatDate(date);
}

/**
* Format date value
*/
Expand All @@ -119,6 +145,10 @@ export function formatDate(value: string | Date, style?: string): string {
const year = String(date.getFullYear()).slice(-2);
return `${month} ${day}, '${year}`;
}

if (style === 'relative') {
return formatRelativeDate(date);
}

// Default format: MMM DD, YYYY
return date.toLocaleDateString('en-US', {
Expand Down
18 changes: 9 additions & 9 deletions packages/fields/src/widgets/GridField.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,39 +12,39 @@ export function GridField({ value, field, readonly, ...props }: FieldWidgetProps
const columns = gridField?.columns || [];

if (!value || !Array.isArray(value)) {
return <span className="text-sm text-gray-500">-</span>;
return <span className="text-sm text-muted-foreground">-</span>;
}

if (readonly) {
return (
<div className={cn("text-sm", props.className)}>
<span className="text-gray-700">{value.length} rows</span>
<span className="text-foreground">{value.length} rows</span>
</div>
);
}

// Simple read-only table view
return (
<div className={cn("border border-gray-200 rounded-md overflow-hidden", props.className)}>
<div className={cn("border border-border rounded-lg overflow-hidden", props.className)}>
<div className="overflow-auto max-h-60">
<table className="w-full text-sm">
<thead className="bg-gray-50 border-b border-gray-200">
<thead className="bg-muted border-b border-border">
<tr>
{columns.map((col: any, idx: number) => (
<th
key={idx}
className="px-3 py-2 text-left text-xs font-medium text-gray-700"
className="px-3 py-2 text-left text-xs font-medium text-muted-foreground"
>
{col.label || col.name}
</th>
))}
</tr>
</thead>
<tbody className="divide-y divide-gray-200">
<tbody className="divide-y divide-border">
{value.slice(0, 5).map((row: any, rowIdx: number) => (
<tr key={rowIdx} className="hover:bg-gray-50">
<tr key={rowIdx} className="hover:bg-muted/50 transition-colors">
Copy link

Copilot AI Feb 24, 2026

Choose a reason for hiding this comment

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

The row hover style uses transition-colors without a duration class, so the transition duration will default to 0s. If you want the intended 150ms hover transition, add duration-150 (to match the rest of the UI consistency changes).

Suggested change
<tr key={rowIdx} className="hover:bg-muted/50 transition-colors">
<tr key={rowIdx} className="hover:bg-muted/50 transition-colors duration-150">

Copilot uses AI. Check for mistakes.
{columns.map((col: any, colIdx: number) => (
<td key={colIdx} className="px-3 py-2 text-gray-900">
<td key={colIdx} className="px-3 py-2 text-foreground">
{row[col.name] != null ? String(row[col.name]) : '-'}
</td>
))}
Expand All @@ -54,7 +54,7 @@ export function GridField({ value, field, readonly, ...props }: FieldWidgetProps
</table>
</div>
{value.length > 5 && (
<div className="bg-gray-50 px-3 py-2 text-xs text-gray-500 border-t border-gray-200">
<div className="bg-muted px-3 py-2 text-xs text-muted-foreground border-t border-border">
Showing 5 of {value.length} rows
</div>
)}
Expand Down
2 changes: 1 addition & 1 deletion packages/layout/src/SidebarNav.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ export function SidebarNav({ items, title = "Application", className, collapsibl
<SidebarMenu>
{items.map((item) => (
<SidebarMenuItem key={item.href}>
<SidebarMenuButton asChild isActive={location.pathname === item.href}>
<SidebarMenuButton asChild isActive={location.pathname === item.href} tooltip={item.title}>
<NavLink to={item.href}>
{item.icon && <item.icon />}
<span>{item.title}</span>
Expand Down
6 changes: 3 additions & 3 deletions packages/plugin-grid/src/ObjectGrid.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -708,8 +708,8 @@ export const ObjectGrid: React.FC<ObjectGridProps> = ({
}
return (
<div className="p-4 sm:p-8 text-center">
<div className="inline-block animate-spin rounded-full h-8 w-8 border-b-2 border-gray-900"></div>
<p className="mt-2 text-sm text-gray-600">Loading grid...</p>
<div className="inline-block animate-spin rounded-full h-8 w-8 border-b-2 border-foreground"></div>
<p className="mt-2 text-sm text-muted-foreground">Loading grid...</p>
</div>
);
}
Expand Down Expand Up @@ -909,7 +909,7 @@ export const ObjectGrid: React.FC<ObjectGridProps> = ({
return 'bg-indigo-100 text-indigo-800 border-indigo-300';
if (v.includes('prospecting') || v.includes('new') || v.includes('open'))
return 'bg-purple-100 text-purple-800 border-purple-300';
return 'bg-gray-100 text-gray-800 border-gray-300';
return 'bg-muted text-muted-foreground border-border';
};

// Left border color for card accent based on stage
Expand Down
4 changes: 2 additions & 2 deletions packages/plugin-grid/src/VirtualGrid.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,7 @@ export const VirtualGrid: React.FC<VirtualGridProps> = ({
<div className={className}>
{/* Header */}
<div
className={`grid border-b sticky top-0 bg-background z-10 ${headerClassName}`}
className={`grid border-b sticky top-0 bg-muted/30 z-10 ${headerClassName}`}
style={{
gridTemplateColumns: columns
.map((col) => col.width || '1fr')
Expand All @@ -94,7 +94,7 @@ export const VirtualGrid: React.FC<VirtualGridProps> = ({
{columns.map((column, index) => (
<div
key={index}
className={`px-4 py-2 font-semibold text-sm ${
className={`px-4 py-2 text-[11px] font-semibold uppercase tracking-wider text-muted-foreground/70 ${
column.align === 'center'
? 'text-center'
: column.align === 'right'
Expand Down
Loading