feat: polish invoice editor and viewer UI with custom NumberInput

component

- Create custom NumberInput component with increment/decrement buttons
- Add 0.25 step increments for hours and rates in invoice forms
- Implement emerald-themed styling with hover states and accessibility
- Add keyboard navigation (arrow keys) and proper ARIA support
- Condense invoice editor tax/totals section into efficient grid layout
- Update client dropdown to single-line format (name + email)
- Add fixed footer with floating action bar pattern matching business
  forms
- Redesign invoice viewer with better space utilization and visual
  hierarchy
- Maintain professional appearance and consistent design system
- Fix Next.js 15 params Promise handling across all invoice pages
- Resolve TypeScript compilation errors and type-only imports
This commit is contained in:
2025-07-15 00:29:02 -04:00
parent 89de059501
commit f331136090
79 changed files with 9944 additions and 4223 deletions

View File

@@ -0,0 +1,138 @@
# Responsive Table Examples
This document shows how tables adapt across different screen sizes in the beenvoice application.
## Mobile View (< 640px)
### Invoices Table
- **Visible**: Invoice number, client name, amount, status, actions
- **Hidden**: Issue date, due date (shown on detail view)
- **Features**: Compact spacing, smaller buttons, simplified pagination
### Clients Table
- **Visible**: Name with email, actions
- **Hidden**: Phone, address, created date
- **Icon**: Hidden on mobile to save space
### Businesses Table
- **Visible**: Name with email, actions
- **Hidden**: Phone, address, tax ID, website
- **Icon**: Hidden on mobile to save space
## Tablet View (640px - 1024px)
### Invoices Table
- **Added**: Issue date column
- **Still Hidden**: Due date (less critical than issue date)
- **Features**: Search bar expands, column visibility toggle appears
### Clients Table
- **Added**: Phone column, client icon
- **Still Hidden**: Address, created date
- **Features**: Better spacing, full search functionality
### Businesses Table
- **Added**: Phone column, business icon
- **Still Hidden**: Address, tax ID
- **Features**: Website links become visible
## Desktop View (> 1024px)
### All Tables
- **Full Features**: All columns visible
- **Enhanced**:
- Full pagination controls with page size selector
- Column visibility toggle
- Advanced filters
- Comfortable spacing
- All metadata visible
## Code Examples
### Responsive Column Definition
```tsx
// Hide on mobile, show on tablet and up
{
accessorKey: "phone",
header: ({ column }) => (
<DataTableColumnHeader column={column} title="Phone" />
),
cell: ({ row }) => (
<span className="hidden md:inline">{row.original.phone || "—"}</span>
),
}
// Hide on mobile and tablet, show on desktop
{
id: "address",
header: "Address",
cell: ({ row }) => (
<span className="hidden lg:inline">{formatAddress(row.original)}</span>
),
}
```
### Responsive Cell Content
```tsx
// Icon hidden on mobile
<div className="flex items-center gap-3">
<div className="hidden rounded-lg bg-status-info-muted p-2 sm:flex">
<UserPlus className="h-4 w-4 text-status-info" />
</div>
<div className="min-w-0">
<p className="truncate font-medium">{client.name}</p>
<p className="truncate text-sm text-muted-foreground">
{client.email || "—"}
</p>
</div>
</div>
```
### Responsive Actions
```tsx
// Compact action buttons that work on all screen sizes
<div className="flex items-center justify-end gap-1">
<Button variant="ghost" size="sm" className="h-8 w-8 p-0">
<Pencil className="h-3.5 w-3.5" />
</Button>
<Button variant="ghost" size="sm" className="h-8 w-8 p-0">
<Trash2 className="h-3.5 w-3.5" />
</Button>
</div>
```
## Filter Bar Behavior
### Mobile
- Search input takes full width
- Filter dropdowns stack vertically
- Column visibility hidden
- Clear filters button visible when filters active
### Tablet+
- Search input limited to max-width
- Filter dropdowns in horizontal row
- Column visibility toggle appears
- All controls in single row
## Pagination Behavior
### Mobile
- Simplified page indicator (1/5 format)
- Compact button spacing
- Page size selector with smaller text
### Desktop
- Full "Page 1 of 5" text
- Comfortable button spacing
- First/Last page buttons visible
- Entries count with detailed information
## Best Practices
1. **Priority Content**: Always show the most important data on mobile
2. **Progressive Enhancement**: Add columns as screen size increases
3. **Touch Targets**: Maintain 44px minimum touch targets on mobile
4. **Text Truncation**: Use `truncate` class for long text in narrow columns
5. **Icon Usage**: Hide decorative icons on mobile, keep functional ones
6. **Testing**: Always test at 375px (iPhone SE), 768px (iPad), and 1440px (Desktop)

324
docs/UI_UNIFORMITY_GUIDE.md Normal file
View File

@@ -0,0 +1,324 @@
# UI Uniformity Guide for beenvoice
## Overview
This guide documents the unified component system implemented across the beenvoice application to ensure consistent UI/UX patterns. The system follows a hierarchical approach where:
1. **CSS Variables** (in `globals.css`) define the design tokens
2. **UI Components** (in `components/ui`) consume these variables
3. **Pages** use components with minimal additional styling
## Design System Principles
### 1. Variable-Based Theming
All colors, spacing, and other design tokens are defined as CSS variables in `globals.css`:
- Brand colors: `--brand-primary`, `--brand-secondary`
- Status colors: `--status-success`, `--status-warning`, `--status-error`, `--status-info`
- Semantic colors: `--background`, `--foreground`, `--muted`, etc.
### 2. Component Composition
Complex UI patterns are built from smaller, reusable components rather than duplicating code.
### 3. Minimal Page-Level Styling
Pages should primarily compose pre-built components and avoid custom Tailwind classes where possible.
## Core Unified Components
### Page Layout Components
#### `PageContent`
Wraps page content with consistent spacing:
```tsx
<PageContent spacing="default">
{/* Page sections */}
</PageContent>
```
#### `PageSection`
Groups related content with optional title and actions:
```tsx
<PageSection
title="Section Title"
description="Optional description"
actions={<Button>Action</Button>}
>
{/* Section content */}
</PageSection>
```
#### `PageGrid`
Responsive grid layout with preset column options:
```tsx
<PageGrid columns={3} gap="default">
{/* Grid items */}
</PageGrid>
```
### Data Display Components
#### `DataTable`
Unified table component using @tanstack/react-table with floating card design:
```tsx
import { ColumnDef } from "@tanstack/react-table";
import { DataTable, DataTableColumnHeader } from "~/components/ui/data-table";
import { PageSection } from "~/components/ui/page-layout";
const columns: ColumnDef<DataType>[] = [
{
accessorKey: "name",
header: ({ column }) => (
<DataTableColumnHeader column={column} title="Name" />
),
cell: ({ row }) => {
const name = row.getValue("name") as string;
return <div className="font-medium">{name}</div>;
}
},
{
id: "actions",
cell: ({ row }) => {
const item = row.original;
return (
<Button variant="ghost" size="sm" className="h-8 w-8 p-0">
<Edit className="h-3.5 w-3.5" />
</Button>
);
}
}
];
const filterableColumns = [
{
id: "status",
title: "Status",
options: [
{ label: "Active", value: "active" },
{ label: "Inactive", value: "inactive" }
]
}
];
// Wrap in PageSection for title/description
<PageSection
title="Table Title"
description="Optional description"
>
<DataTable
columns={columns}
data={data}
searchPlaceholder="Search by name..."
filterableColumns={filterableColumns}
/>
</PageSection>
```
Features:
- **Floating Card Design**: Three separate cards for filter bar, table content, and pagination
- **Filter Bar Card**: Minimal padding (p-3) with global search and column filters
- **Table Content Card**: Clean borders with overflow handling
- **Pagination Card**: Compact controls with page size selector
- **Responsive Design**: Mobile-optimized with hidden columns on smaller screens
- **Tight Appearance**: Compact spacing with smaller action buttons
- **Sorting**: Visual indicators with proper arrow directions
- **Column Visibility**: Toggle columns (hidden on mobile)
- **Dark Mode**: Consistent styling across light/dark themes
- **Loading States**: DataTableSkeleton component with matching card structure
#### `StatsCard`
Displays statistics with consistent styling:
```tsx
<StatsCard
title="Total Revenue"
value="$10,000"
icon={DollarSign}
description="From 50 invoices"
variant="success"
/>
```
#### `QuickActionCard`
Interactive cards for navigation or actions:
```tsx
<QuickActionCard
title="Create Invoice"
description="Start a new invoice"
icon={Plus}
variant="success"
>
<Link href="/invoices/new">
<div className="h-full w-full" />
</Link>
</QuickActionCard>
```
### Feedback Components
#### `EmptyState`
Consistent empty state displays:
```tsx
<EmptyState
icon={<FileText className="h-8 w-8" />}
title="No invoices yet"
description="Create your first invoice to get started"
action={<Button>Create Invoice</Button>}
/>
```
## Component Variants
### Color Variants
Most components support these variants:
- `default` - Uses default theme colors
- `success` - Green color scheme for positive states
- `warning` - Orange/amber for warnings
- `error` - Red for errors or destructive actions
- `info` - Blue for informational content
### Size Variants
- `sm` - Small size
- `default` - Normal size
- `lg` - Large size
## Usage Examples
### Standard Page Structure
```tsx
export default function ExamplePage() {
return (
<PageContent>
<PageHeader
title="Page Title"
description="Page description"
variant="gradient"
>
<Button variant="brand">
Primary Action
</Button>
</PageHeader>
<PageSection>
<PageGrid columns={4}>
<StatsCard {...statsProps} />
</PageGrid>
</PageSection>
<PageSection
title="Data Table Title"
description="Table description"
>
<DataTable {...tableProps} />
</PageSection>
</PageContent>
);
}
```
### Consistent Button Usage
```tsx
// Primary actions
<Button variant="brand">Create New</Button>
// Secondary actions
<Button variant="outline">Cancel</Button>
// Destructive actions
<Button variant="destructive">Delete</Button>
// Icon-only actions
<Button variant="ghost" size="icon">
<Edit className="h-4 w-4" />
</Button>
```
## Styling Guidelines
### Do's
- ✅ Use predefined color variables from globals.css
- ✅ Compose existing UI components
- ✅ Use semantic variant names (success, error, etc.)
- ✅ Follow the established spacing patterns
- ✅ Use the PageLayout components for structure
### Don'ts
- ❌ Add custom colors directly in components
- ❌ Create one-off table or card implementations
- ❌ Override component styles with important flags
- ❌ Use arbitrary spacing values
- ❌ Mix different UI patterns on the same page
## Migration Checklist
When updating a page to use the unified system:
1. Replace custom tables with `DataTable` using @tanstack/react-table ColumnDef
2. Replace statistics displays with `StatsCard`
3. Replace action cards with `QuickActionCard`
4. Wrap content in `PageContent` and `PageSection`
5. Use `PageGrid` for responsive layouts
6. Replace custom empty states with `EmptyState`
7. Update buttons to use the `brand` variant for primary actions
8. Remove page-specific color classes
9. Use `DataTableColumnHeader` for sortable column headers
10. Use `DataTableSkeleton` for loading states
## Color System Reference
### Brand Colors
- Primary: Green (`#16a34a` / `oklch(0.646 0.222 164.25)`)
- Secondary: Teal/cyan shades
- Gradients: Use `bg-brand-gradient` class
### Status Colors
- Success: Green shades
- Warning: Amber/orange shades
- Error: Red shades
- Info: Blue shades
### Semantic Colors
- Background: White/dark gray
- Foreground: Black/white text
- Muted: Gray shades for secondary content
- Border: Light gray borders
## Component Documentation
For detailed component APIs and props, refer to:
- `/src/components/ui/data-table.tsx` - TanStack Table-based data table with sorting, filtering, and pagination
- `/src/components/ui/stats-card.tsx` - Statistics display cards
- `/src/components/ui/quick-action-card.tsx` - Interactive action cards
- `/src/components/ui/page-layout.tsx` - Page structure components
### DataTable Props
- `columns`: ColumnDef array from @tanstack/react-table
- `data`: Array of data to display
- `searchPlaceholder?`: Placeholder text for search input
- `showColumnVisibility?`: Show/hide column visibility toggle (default: true)
- `showPagination?`: Show/hide pagination controls (default: true)
- `showSearch?`: Show/hide search input (default: true)
- `pageSize?`: Number of items per page (default: 10)
- `filterableColumns?`: Array of column filters with options
Note: `title` and `description` should be provided via the wrapping `PageSection` component for consistent spacing and typography.
### Responsive Table Guidelines
- Use `hidden sm:flex` classes for icons in table cells
- Use `hidden md:inline` for less important columns on mobile
- Use `min-w-0` and `truncate` for text that might overflow
- Keep action buttons small with `h-8 w-8 p-0` sizing
- Test tables at all breakpoints (mobile, tablet, desktop)
## Future Considerations
1. **Form Components**: Create unified form field components
2. **Modal Patterns**: Standardize modal and dialog usage
3. **Loading States**: Create consistent skeleton loaders
4. **Animation**: Define standard transition patterns
5. **Icons**: Establish icon usage guidelines
## Maintenance
To maintain UI consistency:
1. Always check for existing components before creating new ones
2. Update this guide when adding new unified components
3. Review PRs for adherence to these patterns
4. Refactor pages that deviate from the system

198
docs/breadcrumbs-guide.md Normal file
View File

@@ -0,0 +1,198 @@
# Dynamic Breadcrumbs Guide
## Overview
The breadcrumb system in beenvoice automatically generates navigation trails based on the current URL path. It features intelligent pluralization, proper capitalization, and dynamic resource name fetching.
## Key Features
### 1. Automatic Pluralization
The breadcrumb system intelligently handles singular and plural forms:
- **List pages** (e.g., `/dashboard/businesses`) → "Businesses"
- **Detail pages** (e.g., `/dashboard/businesses/[id]`) → "Business"
- **New pages** (e.g., `/dashboard/businesses/new`) → "Business" (singular context)
### 2. Smart Capitalization
All route segments are automatically capitalized:
- `businesses` → "Businesses"
- `clients` → "Clients"
- `invoices` → "Invoices"
### 3. Dynamic Resource Names
Instead of showing UUIDs, breadcrumbs fetch and display actual resource names:
- `/dashboard/clients/123e4567-e89b-12d3-a456-426614174000` → "Dashboard / Clients / John Doe"
- `/dashboard/invoices/987fcdeb-51a2-43f1-b321-123456789abc` → "Dashboard / Invoices / INV-2024-001"
### 4. Context-Aware Labels
Special pages are handled intelligently:
- **Edit pages**: Show the resource name instead of "Edit" as the last breadcrumb
- **New pages**: Show "New" as the last breadcrumb
- **Import/Export pages**: Show appropriate action labels
## Implementation Details
### Pluralization Rules
The system uses a comprehensive pluralization utility (`src/lib/pluralize.ts`) that handles:
```typescript
// Common business terms
business businesses
client clients
invoice invoices
category categories
company companies
// General rules
- Words ending in 's', 'ss', 'sh', 'ch', 'x', 'z' add 'es'
- Words ending in consonant + 'y' change to 'ies'
- Words ending in 'f' or 'fe' change to 'ves'
- Default add 's'
```
### Resource Fetching
The breadcrumbs automatically detect resource IDs and fetch the appropriate data:
```typescript
// Detects UUID patterns in the URL
const isUUID = /^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$/
// Fetches data based on resource type
- Clients: Shows client name
- Invoices: Shows invoice number or formatted date
- Businesses: Shows business name
```
### Loading States
While fetching resource data, breadcrumbs show loading skeletons:
```tsx
<Skeleton className="inline-block h-5 w-24 align-middle" />
```
## Usage Examples
### Basic List Page
**URL**: `/dashboard/clients`
**Breadcrumbs**: Dashboard / Clients
### Resource Detail Page
**URL**: `/dashboard/clients/550e8400-e29b-41d4-a716-446655440000`
**Breadcrumbs**: Dashboard / Clients / Jane Smith
### Resource Edit Page
**URL**: `/dashboard/businesses/550e8400-e29b-41d4-a716-446655440000/edit`
**Breadcrumbs**: Dashboard / Businesses / Acme Corp
*(Note: "Edit" is hidden when showing the resource name)*
### New Resource Page
**URL**: `/dashboard/invoices/new`
**Breadcrumbs**: Dashboard / Invoices / New
### Nested Resources
**URL**: `/dashboard/clients/550e8400-e29b-41d4-a716-446655440000/invoices`
**Breadcrumbs**: Dashboard / Clients / John Doe / Invoices
## Customization
### Adding New Resource Types
To add a new resource type, update the pluralization rules:
```typescript
// In src/lib/pluralize.ts
const PLURALIZATION_RULES = {
// ... existing rules
product: { singular: "Product", plural: "Products" },
service: { singular: "Service", plural: "Services" },
};
```
### Custom Resource Labels
For resources that need custom display logic, add to the breadcrumb component:
```typescript
// For invoices, show invoice number instead of ID
if (prevSegment === "invoices") {
label = invoice.invoiceNumber || format(new Date(invoice.issueDate), "MMM dd, yyyy");
}
```
### Special Segments
Add new special segments to the `SPECIAL_SEGMENTS` object:
```typescript
const SPECIAL_SEGMENTS = {
new: "New",
edit: "Edit",
import: "Import",
export: "Export",
duplicate: "Duplicate",
archive: "Archive",
};
```
## Best Practices
1. **Consistent Naming**: Use consistent URL patterns across your app
- List pages: `/dashboard/[resource]`
- Detail pages: `/dashboard/[resource]/[id]`
- Actions: `/dashboard/[resource]/[id]/[action]`
2. **Resource Fetching**: Only fetch data when needed
- Check resource type before enabling queries
- Use proper loading states
3. **Error Handling**: Handle cases where resources don't exist
- Show fallback text or maintain UUID display
- Don't break the breadcrumb trail
4. **Performance**: Breadcrumb queries are lightweight
- Only fetch minimal data (id, name)
- Use React Query caching effectively
## API Integration
The breadcrumb component integrates with tRPC routers:
```typescript
// Each resource router should have a getById method
getById: protectedProcedure
.input(z.object({ id: z.string() }))
.query(async ({ ctx, input }) => {
// Return resource with at least id and name/title
})
```
## Accessibility
- Breadcrumbs use semantic HTML with proper ARIA labels
- Each segment is a link except the current page
- Proper keyboard navigation support
- Screen reader friendly with role="navigation"
## Responsive Design
- Breadcrumbs wrap on smaller screens
- Font sizes adjust: `text-sm sm:text-base`
- Separators scale appropriately
- Loading skeletons match text size
## Migration from Static Breadcrumbs
If migrating from hardcoded breadcrumbs:
1. Remove static breadcrumb definitions
2. Ensure URLs follow consistent patterns
3. Add getById methods to resource routers
4. Update imports to use `DashboardBreadcrumbs`
The dynamic system will automatically generate appropriate breadcrumbs based on the URL structure.

View File

@@ -0,0 +1,154 @@
# Data Table Improvements Summary
## Overview
The data table component has been significantly improved to address padding, scaling, and responsiveness issues. The tables now provide a cleaner, more compact appearance while maintaining excellent usability across all device sizes.
## Key Improvements Made
### 1. Tighter, More Consistent Padding
**Before:**
- Inconsistent padding across different table sections
- Excessive vertical padding making tables feel loose
- Cards had default py-6 padding that was too spacious
**After:**
- Table cells: `py-1.5` (mobile) / `py-2` (desktop) - reduced from `py-2.5` / `py-3`
- Table headers: `h-9` (mobile) / `h-10` (desktop) - reduced from `h-10` / `h-12`
- Filter/pagination cards: `py-2` with `px-3` horizontal padding
- Table card: `p-0` to wrap content tightly
### 2. Improved Responsive Column Handling
**Before:**
```tsx
// Cells would hide but headers remained visible
cell: ({ row }) => (
<span className="hidden md:inline">{row.original.phone}</span>
),
```
**After:**
```tsx
// Both header and cell hide together
cell: ({ row }) => row.original.phone || "—",
meta: {
headerClassName: "hidden md:table-cell",
cellClassName: "hidden md:table-cell",
},
```
### 3. Better Small Card Appearance
- Filter card: Compact `py-2` padding with proper horizontal spacing
- Pagination card: Matching `py-2` padding for consistency
- Content aligned properly within smaller card boundaries
- Removed excessive gaps between elements
- Search box now has consistent padding without extra bottom spacing on mobile
### 4. Responsive Font Sizing
- Base text: `text-xs` on mobile, `text-sm` on desktop
- Consistent scaling across all table elements
- Better readability on small screens without wasting space
## Visual Comparison
### Table Density
- **Before**: ~60px per row with excessive padding
- **After**: ~40px per row with comfortable but efficient spacing
### Card Heights
- **Filter Card**: Reduced from ~80px to ~56px
- **Pagination Card**: Reduced from ~72px to ~48px
- **Table Card**: Now wraps content exactly with no extra space
- **Pagination Layout**: Entry count and pagination controls now stay on the same line on mobile
## Implementation Examples
### Responsive Column Definition
```tsx
const columns: ColumnDef<DataType>[] = [
{
accessorKey: "name",
header: ({ column }) => (
<DataTableColumnHeader column={column} title="Name" />
),
cell: ({ row }) => row.original.name,
// Always visible
},
{
accessorKey: "email",
header: ({ column }) => (
<DataTableColumnHeader column={column} title="Email" />
),
cell: ({ row }) => row.original.email,
meta: {
// Hidden on mobile, visible on tablets and up
headerClassName: "hidden md:table-cell",
cellClassName: "hidden md:table-cell",
},
},
{
accessorKey: "createdAt",
header: ({ column }) => (
<DataTableColumnHeader column={column} title="Created" />
),
cell: ({ row }) => formatDate(row.getValue("createdAt")),
meta: {
// Only visible on large screens
headerClassName: "hidden lg:table-cell",
cellClassName: "hidden lg:table-cell",
},
},
];
```
### Page Header Actions
Page headers now properly position action buttons to the right on all screen sizes:
```tsx
<PageHeader
title="Invoices"
description="Manage your invoices and track payments"
variant="gradient"
>
<Button asChild variant="brand" size="lg">
<Link href="/dashboard/invoices/new">
<Plus className="mr-2 h-5 w-5" /> New Invoice
</Link>
</Button>
</PageHeader>
```
### Breakpoint Reference
- `sm`: 640px and up
- `md`: 768px and up
- `lg`: 1024px and up
- `xl`: 1280px and up
## Benefits
1. **More Data Visible**: Tighter spacing allows more rows to be visible without scrolling
2. **Professional Appearance**: Clean, compact design suitable for business applications
3. **Better Mobile UX**: Properly hidden columns prevent layout breaking
4. **Consistent Styling**: All table instances now follow the same spacing rules
5. **Performance**: CSS-only solution with no JavaScript overhead
6. **Improved Mobile Layout**: Pagination controls stay inline with entry count on mobile
7. **Consistent Header Actions**: Action buttons properly positioned to the right
## Migration Checklist
- [x] Update column definitions to use `meta` properties
- [x] Remove inline responsive classes from cell content
- [x] Test on actual mobile devices
- [x] Verify touch targets remain accessible (min 44x44px)
- [x] Check that critical data remains visible on small screens
## Best Practices Going Forward
1. **Column Priority**: Always keep the most important 2-3 columns visible on mobile
2. **Content Density**: Use the tighter spacing for data tables, looser spacing for content lists
3. **Responsive Testing**: Test at 320px, 768px, and 1024px minimum
4. **Accessibility**: Ensure interactive elements maintain proper touch targets despite tighter spacing

View File

@@ -0,0 +1,246 @@
# Data Table Responsive Design Guide
## Overview
The data table component has been updated to provide better responsive behavior, consistent padding, and proper scaling across different screen sizes.
## Key Improvements
### 1. Consistent Padding
- Uniform padding across all table elements
- Responsive padding that scales with screen size
- Cards now have consistent spacing (p-3 on mobile, p-4 on desktop)
### 2. Proper Responsive Column Hiding
- Columns now properly hide both headers and cells on smaller screens
- Uses `meta` properties for clean column visibility control
- No more orphaned headers on mobile devices
### 3. Better Scaling
- Font sizes adapt to screen size (text-xs on mobile, text-sm on desktop)
- Button sizes and spacing adjust appropriately
- Pagination controls are optimized for touch devices
## Using Responsive Columns
### Basic Column Definition
```tsx
const columns: ColumnDef<YourDataType>[] = [
{
accessorKey: "name",
header: ({ column }) => (
<DataTableColumnHeader column={column} title="Name" />
),
cell: ({ row }) => row.original.name,
// Always visible on all screen sizes
},
{
accessorKey: "phone",
header: ({ column }) => (
<DataTableColumnHeader column={column} title="Phone" />
),
cell: ({ row }) => row.original.phone || "—",
meta: {
// Hidden on mobile, visible on md screens and up
headerClassName: "hidden md:table-cell",
cellClassName: "hidden md:table-cell",
},
},
{
accessorKey: "address",
header: "Address",
cell: ({ row }) => formatAddress(row.original),
meta: {
// Hidden on mobile and tablet, visible on lg screens and up
headerClassName: "hidden lg:table-cell",
cellClassName: "hidden lg:table-cell",
},
},
{
accessorKey: "createdAt",
header: ({ column }) => (
<DataTableColumnHeader column={column} title="Created" />
),
cell: ({ row }) => formatDate(row.getValue("createdAt")),
meta: {
// Only visible on xl screens and up
headerClassName: "hidden xl:table-cell",
cellClassName: "hidden xl:table-cell",
},
},
];
```
### Responsive Breakpoints
- **Always visible**: No meta properties needed
- **md and up** (768px+): `hidden md:table-cell`
- **lg and up** (1024px+): `hidden lg:table-cell`
- **xl and up** (1280px+): `hidden xl:table-cell`
## Complex Cell Content
For cells with complex content that should partially hide on mobile:
```tsx
{
accessorKey: "client",
header: ({ column }) => (
<DataTableColumnHeader column={column} title="Client" />
),
cell: ({ row }) => {
const client = row.original;
return (
<div className="flex items-center gap-3">
{/* Icon hidden on mobile, shown on sm screens */}
<div className="bg-status-info-muted hidden rounded-lg p-2 sm:flex">
<UserIcon className="text-status-info h-4 w-4" />
</div>
<div className="min-w-0">
<p className="truncate font-medium">{client.name}</p>
{/* Secondary info can be hidden on very small screens if needed */}
<p className="text-muted-foreground truncate text-sm">
{client.email || "—"}
</p>
</div>
</div>
);
},
}
```
## Best Practices
### 1. Priority-Based Column Hiding
- Always show the most important columns (e.g., name, status, primary action)
- Hide supplementary information first (e.g., dates, secondary details)
- Consider hiding decorative elements (icons) on mobile while keeping text
### 2. Mobile-First Design
- Ensure at least 2-3 columns are visible on mobile
- Test on actual devices, not just browser dev tools
- Consider the minimum viable information for each row
### 3. Touch-Friendly Actions
- Action buttons should be at least 44x44px on mobile
- Use appropriate spacing between interactive elements
- Consider grouping actions in a dropdown on mobile
### 4. Performance
- The responsive system uses CSS classes, so there's no JavaScript overhead
- Column visibility is handled by Tailwind's responsive utilities
- No re-renders needed when resizing
## Migration Guide
If you have existing data tables, update them as follows:
### Before:
```tsx
{
accessorKey: "phone",
header: ({ column }) => (
<DataTableColumnHeader column={column} title="Phone" />
),
cell: ({ row }) => (
<span className="hidden md:inline">{row.original.phone || "—"}</span>
),
}
```
### After:
```tsx
{
accessorKey: "phone",
header: ({ column }) => (
<DataTableColumnHeader column={column} title="Phone" />
),
cell: ({ row }) => row.original.phone || "—",
meta: {
headerClassName: "hidden md:table-cell",
cellClassName: "hidden md:table-cell",
},
}
```
## Common Patterns
### Status Columns
Always visible, use color and icons to convey information efficiently:
```tsx
{
accessorKey: "status",
header: ({ column }) => (
<DataTableColumnHeader column={column} title="Status" />
),
cell: ({ row }) => <StatusBadge status={row.original.status} />,
}
```
### Date Columns
Often hidden on mobile, show relative dates when space is limited:
```tsx
{
accessorKey: "createdAt",
header: ({ column }) => (
<DataTableColumnHeader column={column} title="Created" />
),
cell: ({ row }) => {
const date = row.getValue("createdAt") as Date;
return (
<>
{/* Full date on larger screens */}
<span className="hidden sm:inline">{formatDate(date)}</span>
{/* Relative date on mobile */}
<span className="sm:hidden">{formatRelativeDate(date)}</span>
</>
);
},
}
```
### Action Columns
Keep actions accessible but space-efficient:
```tsx
{
id: "actions",
cell: ({ row }) => {
const item = row.original;
return (
<div className="flex items-center justify-end gap-1">
{/* Show individual buttons on larger screens */}
<div className="hidden sm:flex sm:gap-1">
<EditButton item={item} />
<DeleteButton item={item} />
</div>
{/* Dropdown menu on mobile */}
<div className="sm:hidden">
<ActionsDropdown item={item} />
</div>
</div>
);
},
}
```
## Testing Checklist
- [ ] Table is readable on 320px wide screens
- [ ] Headers and cells align properly at all breakpoints
- [ ] Touch targets are at least 44x44px on mobile
- [ ] Horizontal scrolling works smoothly when needed
- [ ] Critical information is always visible
- [ ] Loading states work correctly
- [ ] Empty states are responsive
- [ ] Pagination controls are touch-friendly
## Accessibility Notes
- Hidden columns are properly hidden from screen readers
- Table remains navigable with keyboard at all screen sizes
- Sort controls are accessible on mobile
- Focus indicators are visible on all interactive elements

279
docs/forms-guide.md Normal file
View File

@@ -0,0 +1,279 @@
# Forms Improvement Guide
## Overview
The business and client creation/editing forms have been significantly improved with better organization, shared components, enhanced validation, and improved user experience.
## Key Improvements
### 1. Shared Components & Utilities
#### Address Form Component (`src/components/ui/address-form.tsx`)
A reusable address form component that handles:
- Country-aware formatting (US ZIP codes, Canadian postal codes)
- State dropdown for US addresses, text input for other countries
- Popular countries listed first in country dropdown
- Automatic field adjustments based on country selection
```tsx
<AddressForm
addressLine1={formData.addressLine1}
addressLine2={formData.addressLine2}
city={formData.city}
state={formData.state}
postalCode={formData.postalCode}
country={formData.country}
onChange={handleInputChange}
errors={errors}
required={false}
/>
```
#### Form Constants & Utilities (`src/lib/form-constants.ts`)
Centralized location for:
- US states list with proper formatting
- All countries with ISO codes
- Popular countries for quick selection
- Format functions for phone, postal codes, tax IDs, and URLs
- Validation utilities and messages
### 2. Enhanced Form Validation
#### Real-time Validation
- Errors clear as soon as user starts typing
- Field-specific validation messages
- Visual feedback with red borders on invalid fields
#### Smart Validation Rules
- Email: Proper email format checking
- Phone: US phone number format validation
- Address: Required fields only if any address field is filled
- URL: Automatic https:// prefix addition
```typescript
// Example validation
if (formData.email && !isValidEmail(formData.email)) {
newErrors.email = VALIDATION_MESSAGES.email;
}
```
### 3. Better Form Organization
#### Card-based Sections
Forms are now organized into logical sections using cards:
- **Basic Information**: Core fields like name, tax ID
- **Contact Information**: Email, phone, website
- **Address**: Complete address form with smart country handling
- **Settings**: Business-specific settings like default business flag
#### Consistent Layout
- Maximum width container for better readability
- Responsive grid layouts that stack on mobile
- Proper spacing between sections
- Clear visual hierarchy
### 4. Improved User Experience
#### Loading States
- Skeleton loader while fetching data in edit mode
- Disabled form fields during submission
- Loading spinner in submit button
#### Unsaved Changes Warning
```typescript
const handleCancel = () => {
if (isDirty) {
const confirmed = window.confirm(
"You have unsaved changes. Are you sure you want to leave?"
);
if (!confirmed) return;
}
router.push("/dashboard/businesses");
};
```
#### Smart Field Formatting
- Phone numbers: Auto-format as (555) 123-4567
- Tax ID: Auto-format as 12-3456789
- Postal codes: Format based on country (US vs Canadian)
- Website URLs: Auto-add https:// if missing
### 5. Responsive Design
#### Mobile Optimizations
- Form sections stack vertically on small screens
- Touch-friendly input sizes
- Proper button positioning
- Readable font sizes
#### Desktop Enhancements
- Two-column layouts for related fields
- Optimal reading width
- Side-by-side form actions
### 6. Code Reusability
#### Shared Between Business & Client Forms
- Address form component
- Validation logic
- Format functions
- Constants (states, countries)
- Error handling patterns
#### TypeScript Interfaces
```typescript
interface FormData {
name: string;
email: string;
phone: string;
// ... other fields
}
interface FormErrors {
name?: string;
email?: string;
// ... validation errors
}
```
## Usage Examples
### Basic Form Implementation
```tsx
export function BusinessForm({ businessId, mode }: BusinessFormProps) {
const [formData, setFormData] = useState<FormData>(initialFormData);
const [errors, setErrors] = useState<FormErrors>({});
const [isSubmitting, setIsSubmitting] = useState(false);
const [isDirty, setIsDirty] = useState(false);
// Handle input changes
const handleInputChange = (field: string, value: string | boolean) => {
setFormData((prev) => ({ ...prev, [field]: value }));
setIsDirty(true);
// Clear error when user types
if (errors[field as keyof FormErrors]) {
setErrors((prev) => ({ ...prev, [field]: undefined }));
}
};
// Validate and submit
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
if (!validateForm()) {
toast.error("Please correct the errors in the form");
return;
}
// Submit logic...
};
}
```
### Field with Icon and Validation
```tsx
<div className="space-y-2">
<Label htmlFor="email">
Email
<span className="text-muted-foreground ml-1 text-xs">(Optional)</span>
</Label>
<div className="relative">
<Mail className="absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2 text-muted-foreground" />
<Input
id="email"
type="email"
value={formData.email}
onChange={(e) => handleInputChange("email", e.target.value)}
placeholder={PLACEHOLDERS.email}
className={`pl-10 ${errors.email ? "border-destructive" : ""}`}
disabled={isSubmitting}
/>
</div>
{errors.email && (
<p className="text-sm text-destructive">{errors.email}</p>
)}
</div>
```
## Best Practices
### 1. Form State Management
- Use controlled components for all inputs
- Track dirty state for unsaved changes warnings
- Clear errors when user corrects them
- Disable form during submission
### 2. Validation Strategy
- Validate on submit, not on blur (less annoying)
- Clear errors immediately when user starts fixing them
- Show field-level errors below each input
- Use consistent error message format
### 3. Accessibility
- Proper label associations with htmlFor
- Required field indicators
- Error messages linked to fields
- Keyboard navigation support
- Focus management
### 4. Performance
- Memoize expensive computations
- Use debouncing for format functions if needed
- Lazy load country lists
- Optimize re-renders with proper state management
## Migration Guide
### From Old Forms
1. Replace inline state/country arrays with imported constants
2. Use `AddressForm` component instead of individual address fields
3. Apply format functions from `form-constants.ts`
4. Update validation to use shared utilities
5. Wrap sections in Card components
6. Add loading and dirty state tracking
### Example Migration
```tsx
// Before
const US_STATES = [
{ value: "AL", label: "Alabama" },
// ... duplicated in each form
];
// After
import { US_STATES, formatPhoneNumber } from "~/lib/form-constants";
import { AddressForm } from "~/components/ui/address-form";
```
## Future Enhancements
### Planned Improvements
1. **Field-level permissions**: Disable fields based on user role
2. **Auto-save**: Save draft as user types
3. **Multi-step forms**: Break long forms into steps
4. **Conditional fields**: Show/hide fields based on other values
5. **Bulk operations**: Create multiple records at once
6. **Import from templates**: Pre-fill common business types
### Extensibility
The form system is designed to be easily extended:
- Add new format functions to `form-constants.ts`
- Create additional shared form components
- Extend validation rules as needed
- Add new field types with consistent patterns
## Troubleshooting
### Common Issues
1. **Validation not working**: Ensure field names match FormErrors interface
2. **Format function not applying**: Check that onChange uses the format function
3. **Country dropdown not searching**: Verify SearchableSelect has search enabled
4. **Address validation failing**: Check if country field affects validation rules
### Debug Tips
- Use React DevTools to inspect form state
- Check console for validation errors
- Verify API responses match expected format
- Test with different country selections