diff --git a/ROADMAP.md b/ROADMAP.md index 4dffc6e8d..2597e5efb 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -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 `` (`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] `` 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) diff --git a/packages/components/src/custom/config-panel-renderer.tsx b/packages/components/src/custom/config-panel-renderer.tsx index be2284b4b..09ad8d212 100644 --- a/packages/components/src/custom/config-panel-renderer.tsx +++ b/packages/components/src/custom/config-panel-renderer.tsx @@ -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'; @@ -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, )} > @@ -167,13 +168,14 @@ export function ConfigPanelRenderer({ {/* ── Scrollable sections ────────────────────────────── */}
- {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 (
+ {sectionIdx > 0 && } {
{caption && {caption}} - + {selectable && ( - 0 && "sticky left-0 z-20")}> + 0 && "sticky left-0 z-20")}> { )} {showRowNumbers && ( - 0 && "sticky z-20")} style={frozenColumns > 0 ? { left: selectable ? 48 : 0 } : undefined}> + 0 && "sticky z-20")} style={frozenColumns > 0 ? { left: selectable ? 48 : 0 } : undefined}> # )} @@ -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)]', )} @@ -692,7 +692,7 @@ const DataTableRenderer = ({ schema }: { schema: DataTableSchema }) => { {col.headerIcon && ( {col.headerIcon} )} - {col.header} + {col.header} {sortable && col.sortable !== false && getSortIcon(col.accessorKey)} {resizableColumns && col.resizable !== false && ( @@ -707,7 +707,7 @@ const DataTableRenderer = ({ schema }: { schema: DataTableSchema }) => { ); })} {rowActions && ( - Actions + Actions )} diff --git a/packages/components/src/ui/sidebar.tsx b/packages/components/src/ui/sidebar.tsx index 3c43175ac..914e3d4f3 100644 --- a/packages/components/src/ui/sidebar.tsx +++ b/packages/components/src/ui/sidebar.tsx @@ -457,7 +457,7 @@ const SidebarGroupLabel = React.forwardRef< ref={ref} data-sidebar="group-label" className={cn( - "flex h-8 shrink-0 items-center rounded-md px-2 text-xs font-medium text-sidebar-foreground/70 outline-none ring-sidebar-ring transition-[margin,opacity] duration-200 ease-linear focus-visible:ring-2 [&>svg]:size-4 [&>svg]:shrink-0", + "flex h-8 shrink-0 items-center rounded-md px-2 text-xs font-medium text-sidebar-foreground/70 outline-none ring-sidebar-ring transition-[margin,opacity] duration-200 ease-linear focus-visible:ring-2 [&>svg]:size-4 [&>svg]:shrink-0 border-t border-border/30 pt-3 mt-2 first:border-t-0 first:pt-0 first:mt-0", "group-data-[collapsible=icon]:-mt-8 group-data-[collapsible=icon]:opacity-0", className )} @@ -530,7 +530,17 @@ const SidebarMenuItem = React.forwardRef< SidebarMenuItem.displayName = "SidebarMenuItem" const sidebarMenuButtonVariants = cva( - "peer/menu-button flex w-full items-center gap-2 overflow-hidden rounded-md p-2 text-left text-sm outline-none ring-sidebar-ring transition-[width,height,padding] hover:bg-sidebar-accent hover:text-sidebar-accent-foreground focus-visible:ring-2 active:bg-sidebar-accent active:text-sidebar-accent-foreground disabled:pointer-events-none disabled:opacity-50 group-has-[[data-sidebar=menu-action]]/menu-item:pr-8 aria-disabled:pointer-events-none aria-disabled:opacity-50 data-[active=true]:bg-sidebar-accent data-[active=true]:font-medium data-[active=true]:text-sidebar-accent-foreground data-[state=open]:hover:bg-sidebar-accent data-[state=open]:hover:text-sidebar-accent-foreground group-data-[collapsible=icon]:!size-8 group-data-[collapsible=icon]:!p-2 [&>span:last-child]:truncate [&>svg]:size-4 [&>svg]:shrink-0", + [ + "peer/menu-button flex w-full items-center gap-2 overflow-hidden rounded-md p-2 text-left text-sm outline-none ring-sidebar-ring transition-[width,height,padding,color,background-color] duration-150", + "hover:bg-sidebar-accent hover:text-sidebar-accent-foreground focus-visible:ring-2 active:bg-sidebar-accent active:text-sidebar-accent-foreground", + "disabled:pointer-events-none disabled:opacity-50 group-has-[[data-sidebar=menu-action]]/menu-item:pr-8 aria-disabled:pointer-events-none aria-disabled:opacity-50", + "data-[active=true]:bg-sidebar-accent data-[active=true]:font-medium data-[active=true]:text-sidebar-accent-foreground", + "data-[state=open]:hover:bg-sidebar-accent data-[state=open]:hover:text-sidebar-accent-foreground", + "group-data-[collapsible=icon]:!size-8 group-data-[collapsible=icon]:!p-2 [&>span:last-child]:truncate [&>svg]:size-4 [&>svg]:shrink-0", + // Active indicator bar (3px left accent) + "relative data-[active=true]:before:absolute data-[active=true]:before:left-0 data-[active=true]:before:top-1/2 data-[active=true]:before:-translate-y-1/2", + "data-[active=true]:before:h-4 data-[active=true]:before:w-[3px] data-[active=true]:before:rounded-full data-[active=true]:before:bg-primary", + ].join(" "), { variants: { variant: { diff --git a/packages/fields/src/index.tsx b/packages/fields/src/index.tsx index cfa165305..7891a65f6 100644 --- a/packages/fields/src/index.tsx +++ b/packages/fields/src/index.tsx @@ -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") + */ +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 */ @@ -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', { diff --git a/packages/fields/src/widgets/GridField.tsx b/packages/fields/src/widgets/GridField.tsx index 658ee41a5..a629d32e0 100644 --- a/packages/fields/src/widgets/GridField.tsx +++ b/packages/fields/src/widgets/GridField.tsx @@ -12,39 +12,39 @@ export function GridField({ value, field, readonly, ...props }: FieldWidgetProps const columns = gridField?.columns || []; if (!value || !Array.isArray(value)) { - return -; + return -; } if (readonly) { return (
- {value.length} rows + {value.length} rows
); } // Simple read-only table view return ( -
+
- + {columns.map((col: any, idx: number) => ( ))} - + {value.slice(0, 5).map((row: any, rowIdx: number) => ( - + {columns.map((col: any, colIdx: number) => ( - ))} @@ -54,7 +54,7 @@ export function GridField({ value, field, readonly, ...props }: FieldWidgetProps
{col.label || col.name}
+ {row[col.name] != null ? String(row[col.name]) : '-'}
{value.length > 5 && ( -
+
Showing 5 of {value.length} rows
)} diff --git a/packages/layout/src/SidebarNav.tsx b/packages/layout/src/SidebarNav.tsx index 7a83d7fb2..70b0051b4 100644 --- a/packages/layout/src/SidebarNav.tsx +++ b/packages/layout/src/SidebarNav.tsx @@ -36,7 +36,7 @@ export function SidebarNav({ items, title = "Application", className, collapsibl {items.map((item) => ( - + {item.icon && } {item.title} diff --git a/packages/plugin-grid/src/ObjectGrid.tsx b/packages/plugin-grid/src/ObjectGrid.tsx index c02e41a82..43615bb0c 100644 --- a/packages/plugin-grid/src/ObjectGrid.tsx +++ b/packages/plugin-grid/src/ObjectGrid.tsx @@ -708,8 +708,8 @@ export const ObjectGrid: React.FC = ({ } return (
-
-

Loading grid...

+
+

Loading grid...

); } @@ -909,7 +909,7 @@ export const ObjectGrid: React.FC = ({ 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 diff --git a/packages/plugin-grid/src/VirtualGrid.tsx b/packages/plugin-grid/src/VirtualGrid.tsx index bbefc8f2d..603a51d18 100644 --- a/packages/plugin-grid/src/VirtualGrid.tsx +++ b/packages/plugin-grid/src/VirtualGrid.tsx @@ -84,7 +84,7 @@ export const VirtualGrid: React.FC = ({
{/* Header */}
col.width || '1fr') @@ -94,7 +94,7 @@ export const VirtualGrid: React.FC = ({ {columns.map((column, index) => (
= ({ return fields; }, [objectDef, schema.fields, schema.filterableFields]); - const [searchExpanded, setSearchExpanded] = React.useState(false); - // Quick filter toggle handler const toggleQuickFilter = React.useCallback((id: string) => { setActiveQuickFilters(prev => { @@ -1012,7 +1010,7 @@ export const ListView: React.FC = ({ {/* Airtable-style Toolbar — Row 2: Tool buttons */}
-
+
{/* Hide Fields */} {toolbarFlags.showHideFields && ( @@ -1021,7 +1019,7 @@ export const ListView: React.FC = ({ variant="ghost" size="sm" className={cn( - "h-7 px-2 text-muted-foreground hover:text-primary text-xs", + "h-7 px-2 text-muted-foreground hover:text-primary text-xs transition-colors duration-150", hiddenFields.size > 0 && "text-primary" )} > @@ -1080,8 +1078,8 @@ export const ListView: React.FC = ({ variant="ghost" size="sm" className={cn( - "h-7 px-2 text-muted-foreground hover:text-primary text-xs", - hasFilters && "text-primary" + "h-7 px-2 text-muted-foreground hover:text-primary text-xs transition-colors duration-150", + hasFilters && "bg-primary/10 border border-primary/20 text-primary" )} > @@ -1119,8 +1117,8 @@ export const ListView: React.FC = ({ variant="ghost" size="sm" className={cn( - "h-7 px-2 text-muted-foreground hover:text-primary text-xs", - groupingConfig && "text-primary" + "h-7 px-2 text-muted-foreground hover:text-primary text-xs transition-colors duration-150", + groupingConfig && "bg-primary/10 border border-primary/20 text-primary" )} > @@ -1179,8 +1177,8 @@ export const ListView: React.FC = ({ variant="ghost" size="sm" className={cn( - "h-7 px-2 text-muted-foreground hover:text-primary text-xs", - currentSort.length > 0 && "text-primary" + "h-7 px-2 text-muted-foreground hover:text-primary text-xs transition-colors duration-150", + currentSort.length > 0 && "bg-primary/10 border border-primary/20 text-primary" )} > @@ -1218,8 +1216,8 @@ export const ListView: React.FC = ({ variant="ghost" size="sm" className={cn( - "h-7 px-2 text-muted-foreground hover:text-primary text-xs", - rowColorConfig && "text-primary" + "h-7 px-2 text-muted-foreground hover:text-primary text-xs transition-colors duration-150", + rowColorConfig && "bg-primary/10 border border-primary/20 text-primary" )} > @@ -1283,7 +1281,7 @@ export const ListView: React.FC = ({ + {searchTerm && ( + + )}
- ) : ( - - ))} + )}
@@ -1474,7 +1456,7 @@ export const ListView: React.FC = ({