From fd20f2a21277872acd41cc08eca2669a160fba2e Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 24 Feb 2026 03:53:07 +0000 Subject: [PATCH 1/9] Initial plan From 36d8e61d93fa4e65cc3c37f3e09b9ea01e7a0091 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 24 Feb 2026 04:08:25 +0000 Subject: [PATCH 2/9] feat: replace hardcoded gray colors with theme tokens, add global font-family and --config-panel-width CSS variable Co-authored-by: hotlong <50353452+hotlong@users.noreply.github.com> --- packages/components/src/index.css | 4 ++++ packages/fields/src/widgets/GridField.tsx | 18 +++++++++--------- packages/plugin-report/src/ReportRenderer.tsx | 8 ++++---- 3 files changed, 17 insertions(+), 13 deletions(-) diff --git a/packages/components/src/index.css b/packages/components/src/index.css index d3b0b1e59..29399256c 100644 --- a/packages/components/src/index.css +++ b/packages/components/src/index.css @@ -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 { 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/plugin-report/src/ReportRenderer.tsx b/packages/plugin-report/src/ReportRenderer.tsx index 0c603948f..826297369 100644 --- a/packages/plugin-report/src/ReportRenderer.tsx +++ b/packages/plugin-report/src/ReportRenderer.tsx @@ -54,7 +54,7 @@ export const ReportRenderer: React.FC = ({ schema }) => { } return ( -
+
); @@ -62,7 +62,7 @@ export const ReportRenderer: React.FC = ({ schema }) => { {/* Render Data Grid Section */} {data && data.length > 0 && ( -
+
{(() => { const GridComponent = showGrid ? (ComponentRegistry.get('aggrid') || ComponentRegistry.get('table')) : null; return GridComponent ? ( @@ -78,7 +78,7 @@ export const ReportRenderer: React.FC = ({ schema }) => { // Simple Fallback Table if Grid plugin missing
- + {columns?.map((col: any) => ( @@ -87,7 +87,7 @@ export const ReportRenderer: React.FC = ({ schema }) => { {data.map((row: any, i: number) => ( - + {columns?.map((col: any) => ( ))} From eb0f176b5fabca2b4dd55fb8bc241a93300a803b Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 24 Feb 2026 04:09:57 +0000 Subject: [PATCH 3/9] feat: sidebar active indicator bar, group label separators, tooltip support, config panel separators and CSS variable width, tab bar improvements Co-authored-by: hotlong <50353452+hotlong@users.noreply.github.com> --- .../components/src/custom/config-panel-renderer.tsx | 6 ++++-- packages/components/src/ui/sidebar.tsx | 4 ++-- packages/layout/src/SidebarNav.tsx | 2 +- packages/plugin-list/src/components/TabBar.tsx | 11 ++++++++--- 4 files changed, 15 insertions(+), 8 deletions(-) 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 && } 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,7 @@ 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] 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 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", { variants: { variant: { 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-list/src/components/TabBar.tsx b/packages/plugin-list/src/components/TabBar.tsx index efc224ac5..254b5b1f1 100644 --- a/packages/plugin-list/src/components/TabBar.tsx +++ b/packages/plugin-list/src/components/TabBar.tsx @@ -87,7 +87,7 @@ export const TabBar: React.FC = ({ return (
@@ -97,9 +97,14 @@ export const TabBar: React.FC = ({ return (
{col.headerName || col.label || col.field}
{row[col.field]}
{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/fields/src/index.tsx b/packages/fields/src/index.tsx index cfa165305..feb502ec6 100644 --- a/packages/fields/src/index.tsx +++ b/packages/fields/src/index.tsx @@ -119,6 +119,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', { @@ -128,6 +132,32 @@ export function formatDate(value: string | Date, style?: string): string { }); } +/** + * 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 datetime value */ 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) => (
Date: Tue, 24 Feb 2026 04:14:50 +0000 Subject: [PATCH 5/9] feat: ListView toolbar - inline search input, activated state for Filter/Sort/Group/Color buttons, responsive overflow Co-authored-by: hotlong <50353452+hotlong@users.noreply.github.com> --- packages/plugin-list/src/ListView.tsx | 52 ++++++++++----------------- 1 file changed, 18 insertions(+), 34 deletions(-) diff --git a/packages/plugin-list/src/ListView.tsx b/packages/plugin-list/src/ListView.tsx index cac87a6c1..443bb6103 100644 --- a/packages/plugin-list/src/ListView.tsx +++ b/packages/plugin-list/src/ListView.tsx @@ -1012,7 +1012,7 @@ export const ListView: React.FC = ({ {/* Airtable-style Toolbar — Row 2: Tool buttons */}
-
+
{/* Hide Fields */} {toolbarFlags.showHideFields && ( @@ -1081,7 +1081,7 @@ export const ListView: React.FC = ({ size="sm" className={cn( "h-7 px-2 text-muted-foreground hover:text-primary text-xs", - hasFilters && "text-primary" + hasFilters && "bg-primary/10 border border-primary/20 text-primary" )} > @@ -1120,7 +1120,7 @@ export const ListView: React.FC = ({ size="sm" className={cn( "h-7 px-2 text-muted-foreground hover:text-primary text-xs", - groupingConfig && "text-primary" + groupingConfig && "bg-primary/10 border border-primary/20 text-primary" )} > @@ -1180,7 +1180,7 @@ export const ListView: React.FC = ({ size="sm" className={cn( "h-7 px-2 text-muted-foreground hover:text-primary text-xs", - currentSort.length > 0 && "text-primary" + currentSort.length > 0 && "bg-primary/10 border border-primary/20 text-primary" )} > @@ -1219,7 +1219,7 @@ export const ListView: React.FC = ({ size="sm" className={cn( "h-7 px-2 text-muted-foreground hover:text-primary text-xs", - rowColorConfig && "text-primary" + rowColorConfig && "bg-primary/10 border border-primary/20 text-primary" )} > @@ -1355,42 +1355,26 @@ export const ListView: React.FC = ({ {/* Search */} {toolbarFlags.showSearch && ( - searchExpanded ? ( -
+
handleSearchChange(e.target.value)} className="pl-7 h-7 text-xs bg-muted/50 border-transparent hover:bg-muted focus:bg-background focus:border-input transition-colors" - autoFocus - onBlur={() => { - if (!searchTerm) setSearchExpanded(false); - }} /> - + {searchTerm && ( + + )}
- ) : ( - - ))} + )}
From 60ee94205844228c16a5839543c2a64d23f61305 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 24 Feb 2026 04:19:59 +0000 Subject: [PATCH 6/9] fix: update ListView tests to match inline search input behavior, remove unused searchExpanded state Co-authored-by: hotlong <50353452+hotlong@users.noreply.github.com> --- packages/plugin-list/src/ListView.tsx | 2 - .../src/__tests__/ListView.test.tsx | 45 +++++++------------ 2 files changed, 17 insertions(+), 30 deletions(-) diff --git a/packages/plugin-list/src/ListView.tsx b/packages/plugin-list/src/ListView.tsx index 443bb6103..1e53bb85b 100644 --- a/packages/plugin-list/src/ListView.tsx +++ b/packages/plugin-list/src/ListView.tsx @@ -888,8 +888,6 @@ export const ListView: React.FC = ({ 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 => { diff --git a/packages/plugin-list/src/__tests__/ListView.test.tsx b/packages/plugin-list/src/__tests__/ListView.test.tsx index 8dcdcc238..fb85babd4 100644 --- a/packages/plugin-list/src/__tests__/ListView.test.tsx +++ b/packages/plugin-list/src/__tests__/ListView.test.tsx @@ -66,7 +66,7 @@ describe('ListView', () => { expect(container).toBeTruthy(); }); - it('should render search button', () => { + it('should render search input', () => { const schema: ListViewSchema = { type: 'list-view', objectName: 'contacts', @@ -75,8 +75,8 @@ describe('ListView', () => { }; renderWithProvider(); - const searchButton = screen.getByRole('button', { name: /search/i }); - expect(searchButton).toBeInTheDocument(); + const searchInput = screen.getByPlaceholderText(/search/i); + expect(searchInput).toBeInTheDocument(); }); it('should expand search and call onSearchChange when search input changes', () => { @@ -90,11 +90,7 @@ describe('ListView', () => { renderWithProvider(); - // Click search button to expand - const searchButton = screen.getByRole('button', { name: /search/i }); - fireEvent.click(searchButton); - - const searchInput = screen.getByPlaceholderText(/find/i); + const searchInput = screen.getByPlaceholderText(/search/i); fireEvent.change(searchInput, { target: { value: 'test' } }); expect(onSearchChange).toHaveBeenCalledWith('test'); }); @@ -201,17 +197,13 @@ describe('ListView', () => { renderWithProvider(); - // Click search button to expand search input - const searchButton = screen.getByRole('button', { name: /search/i }); - fireEvent.click(searchButton); - - const searchInput = screen.getByPlaceholderText(/find/i) as HTMLInputElement; + const searchInput = screen.getByPlaceholderText(/search/i) as HTMLInputElement; // Type in search fireEvent.change(searchInput, { target: { value: 'test' } }); expect(searchInput.value).toBe('test'); - // Find and click clear button (the X button inside the expanded search) + // Find and click clear button (the X button inside the search area) const buttons = screen.getAllByRole('button'); const clearButton = buttons.find(btn => btn.querySelector('svg') !== null && searchInput.value !== '' @@ -603,7 +595,7 @@ describe('ListView', () => { // Toolbar Toggle Visibility // ============================ describe('Toolbar Toggle Visibility', () => { - it('should hide Search button when showSearch is false', () => { + it('should hide Search input when showSearch is false', () => { const schema: ListViewSchema = { type: 'list-view', objectName: 'contacts', @@ -613,10 +605,10 @@ describe('ListView', () => { }; renderWithProvider(); - expect(screen.queryByRole('button', { name: /search/i })).not.toBeInTheDocument(); + expect(screen.queryByPlaceholderText(/search/i)).not.toBeInTheDocument(); }); - it('should show Search button when showSearch is true', () => { + it('should show Search input when showSearch is true', () => { const schema: ListViewSchema = { type: 'list-view', objectName: 'contacts', @@ -626,10 +618,10 @@ describe('ListView', () => { }; renderWithProvider(); - expect(screen.getByRole('button', { name: /search/i })).toBeInTheDocument(); + expect(screen.getByPlaceholderText(/search/i)).toBeInTheDocument(); }); - it('should show Search button when showSearch is undefined (default)', () => { + it('should show Search input when showSearch is undefined (default)', () => { const schema: ListViewSchema = { type: 'list-view', objectName: 'contacts', @@ -638,7 +630,7 @@ describe('ListView', () => { }; renderWithProvider(); - expect(screen.getByRole('button', { name: /search/i })).toBeInTheDocument(); + expect(screen.getByPlaceholderText(/search/i)).toBeInTheDocument(); }); it('should hide Filter button when showFilters is false', () => { @@ -1222,7 +1214,7 @@ describe('ListView', () => { }; renderWithProvider(); - expect(screen.queryByRole('button', { name: /search/i })).not.toBeInTheDocument(); + expect(screen.queryByPlaceholderText(/search/i)).not.toBeInTheDocument(); }); it('should hide Sort when userActions.sort is false', () => { @@ -1274,7 +1266,7 @@ describe('ListView', () => { }; renderWithProvider(); - expect(screen.getByRole('button', { name: /search/i })).toBeInTheDocument(); + expect(screen.getByPlaceholderText(/search/i)).toBeInTheDocument(); expect(screen.getByRole('button', { name: /^sort$/i })).toBeInTheDocument(); expect(screen.getByRole('button', { name: /filter/i })).toBeInTheDocument(); expect(screen.getByTitle(/density/i)).toBeInTheDocument(); @@ -1291,7 +1283,7 @@ describe('ListView', () => { }; renderWithProvider(); - expect(screen.queryByRole('button', { name: /search/i })).not.toBeInTheDocument(); + expect(screen.queryByPlaceholderText(/search/i)).not.toBeInTheDocument(); }); }); @@ -1456,11 +1448,8 @@ describe('ListView', () => { renderWithProvider(); - // Click search button to expand - const searchButton = screen.getByRole('button', { name: /search/i }); - fireEvent.click(searchButton); - - const searchInput = screen.getByPlaceholderText(/find/i); + // Search input is always visible inline + const searchInput = screen.getByPlaceholderText(/search/i); fireEvent.change(searchInput, { target: { value: 'alice' } }); // Wait for debounced fetch From f47677a384dd85015ec4b5cd0ad9a0c8d2512192 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 24 Feb 2026 04:41:03 +0000 Subject: [PATCH 7/9] docs: update ROADMAP.md with P2.7 Platform UI Consistency & Interaction Optimization Co-authored-by: hotlong <50353452+hotlong@users.noreply.github.com> --- ROADMAP.md | 32 ++++++++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/ROADMAP.md b/ROADMAP.md index 4dffc6e8d..6a1264ed4 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -781,6 +781,38 @@ 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` and `ReportRenderer.tsx` replaced with theme tokens (`text-muted-foreground`, `bg-muted`, `border-border`) +- [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 + +**Sidebar Navigation:** +- [x] `SidebarMenuButton` active state enhanced with 3px left indicator bar via `before:` pseudo-element +- [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 + +**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 + +**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` + +**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`) + ### P2.5 PWA & Offline (Real Sync) - [ ] Background sync queue → real server sync (replace simulation) From 3e2c554a74c2f235c38e5e6790b767cacab3aaeb Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 24 Feb 2026 04:43:04 +0000 Subject: [PATCH 8/9] refactor: improve readability of sidebar cva variants and reorder formatRelativeDate before formatDate Co-authored-by: hotlong <50353452+hotlong@users.noreply.github.com> --- packages/components/src/ui/sidebar.tsx | 12 +++++- packages/fields/src/index.tsx | 52 +++++++++++++------------- 2 files changed, 37 insertions(+), 27 deletions(-) diff --git a/packages/components/src/ui/sidebar.tsx b/packages/components/src/ui/sidebar.tsx index aec0a314b..8b936e7b5 100644 --- a/packages/components/src/ui/sidebar.tsx +++ b/packages/components/src/ui/sidebar.tsx @@ -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 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", + [ + "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", + // 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 feb502ec6..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 */ @@ -132,32 +158,6 @@ export function formatDate(value: string | Date, style?: string): string { }); } -/** - * 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 datetime value */ From e5cb719361af36e9e07349a7bee1708760dd7dbc Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 24 Feb 2026 04:55:59 +0000 Subject: [PATCH 9/9] feat: add transition-colors to interactive elements, replace remaining hardcoded grays in ObjectGrid and LayoutRenderer Co-authored-by: hotlong <50353452+hotlong@users.noreply.github.com> --- ROADMAP.md | 9 ++++++++- packages/components/src/ui/sidebar.tsx | 2 +- packages/plugin-grid/src/ObjectGrid.tsx | 6 +++--- packages/plugin-list/src/ListView.tsx | 20 +++++++++---------- .../plugin-list/src/components/TabBar.tsx | 2 +- packages/runner/src/LayoutRenderer.tsx | 2 +- 6 files changed, 24 insertions(+), 17 deletions(-) diff --git a/ROADMAP.md b/ROADMAP.md index 6a1264ed4..2597e5efb 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -786,24 +786,30 @@ The `FlowDesigner` is a canvas-based flow editor that bridges the gap between th > All items from the UI consistency optimization (Issue #749) have been implemented. **Global Theme & Design Tokens:** -- [x] Hardcoded gray colors in `GridField.tsx` and `ReportRenderer.tsx` replaced with theme tokens (`text-muted-foreground`, `bg-muted`, `border-border`) +- [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 @@ -812,6 +818,7 @@ The `FlowDesigner` is a canvas-based flow editor that bridges the gap between th **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) diff --git a/packages/components/src/ui/sidebar.tsx b/packages/components/src/ui/sidebar.tsx index 8b936e7b5..914e3d4f3 100644 --- a/packages/components/src/ui/sidebar.tsx +++ b/packages/components/src/ui/sidebar.tsx @@ -531,7 +531,7 @@ 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]", + "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", 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-list/src/ListView.tsx b/packages/plugin-list/src/ListView.tsx index 1e53bb85b..224e71bdd 100644 --- a/packages/plugin-list/src/ListView.tsx +++ b/packages/plugin-list/src/ListView.tsx @@ -1019,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" )} > @@ -1078,7 +1078,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", hasFilters && "bg-primary/10 border border-primary/20 text-primary" )} > @@ -1117,7 +1117,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", groupingConfig && "bg-primary/10 border border-primary/20 text-primary" )} > @@ -1177,7 +1177,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", currentSort.length > 0 && "bg-primary/10 border border-primary/20 text-primary" )} > @@ -1216,7 +1216,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", rowColorConfig && "bg-primary/10 border border-primary/20 text-primary" )} > @@ -1281,7 +1281,7 @@ export const ListView: React.FC = ({