Skip to content

Commit 531abd9

Browse files
authored
Merge pull request #1216 from objectstack-ai/copilot/scan-and-optimize-console
2 parents fd21cfe + d64c5eb commit 531abd9

17 files changed

+126
-48
lines changed

CHANGELOG.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,16 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
77

88
## [Unreleased]
99

10+
### Improved
11+
12+
- **Console UI design optimization sweep** (`@object-ui/console`): Comprehensive accessibility and design consistency improvements across all major console interfaces:
13+
- **Accessibility (WCAG 2.1 AA)**: Added `aria-label`, `aria-pressed`, `aria-required`, `aria-live`, `role="status"`, `role="link"`, `role="toolbar"`, `role="img"` attributes across HomePage, SystemHubPage, MetadataManagerPage, MetadataDetailPage, AppManagementPage, ProfilePage, SearchResultsPage, AuthPageLayout, and DashboardView. Added keyboard navigation (Enter/Space) to all clickable Card components. Added `<title>` element to SVG logo. Added screen-reader-only `<label>` elements for search inputs.
14+
- **Design tokens**: Replaced hardcoded `bg-blue-50`/`text-blue-700` badge in AppCard with Shadcn `<Badge variant="secondary">` for consistent theming.
15+
- **Shadcn component alignment**: Replaced raw `<input type="checkbox">` elements in AppManagementPage with Shadcn `<Checkbox>` component for consistent styling and accessibility.
16+
- **Spacing consistency**: Standardized responsive padding (`px-4 sm:px-6`) on HomePage, unified grid columns across RecentApps/StarredApps (4 columns at xl), standardized `gap-4` in MetadataManagerPage grid, graduated padding (`p-2 sm:p-4 md:p-6`) in DashboardView.
17+
- **Focus management**: Added `focus-visible:opacity-100` on AppCard favorite button so keyboard users can discover it, added focus rings to search result card links, added `focus-visible:ring-2` to DashboardView widget toolbar buttons and MetadataFormDialog native select.
18+
- **i18n compatibility**: Replaced CSS `capitalize` with programmatic string casing in RecentApps/StarredApps type labels.
19+
1020
### Fixed
1121

1222
- **Home page star/favorite not reactive** (`@object-ui/console`): Migrated `useFavorites` from standalone hook to React Context (`FavoritesProvider`) so all consumers (HomePage, AppCard, AppSidebar, UnifiedSidebar) share a single state instance. Previously, each component calling `useFavorites()` created independent state, so toggling a favorite in AppCard did not trigger re-render in HomePage. localStorage persistence is retained as the storage layer.

apps/console/src/components/AuthPageLayout.tsx

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,10 @@ export function AuthPageLayout({ children }: { children: React.ReactNode }) {
2222
strokeLinecap="round"
2323
strokeLinejoin="round"
2424
className="h-10 w-10"
25+
role="img"
26+
aria-label="ObjectStack logo"
2527
>
28+
<title>ObjectStack</title>
2629
<path d="M15 6v12a3 3 0 1 0 3-3H6a3 3 0 1 0 3 3V6a3 3 0 1 0-3 3h12a3 3 0 1 0-3-3" />
2730
</svg>
2831
<span className="text-2xl font-bold">ObjectStack</span>

apps/console/src/components/DashboardView.tsx

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -391,15 +391,16 @@ export function DashboardView({ dataSource }: { dataSource?: any }) {
391391
<div className="shrink-0 flex items-center gap-1.5">
392392
{/* Add-widget toolbar — visible only in edit mode */}
393393
{configPanelOpen && (
394-
<div className="flex items-center gap-1 mr-2" data-testid="dashboard-widget-toolbar">
394+
<div className="flex items-center gap-1 mr-2" role="toolbar" aria-label="Add widgets" data-testid="dashboard-widget-toolbar">
395395
{WIDGET_TYPES.map(({ type, label, Icon }) => (
396396
<button
397397
key={type}
398398
type="button"
399399
data-testid={`dashboard-add-${type}`}
400400
onClick={() => addWidget(type)}
401-
className="inline-flex items-center gap-1 rounded-md border border-input bg-background px-2 py-1.5 text-[11px] font-medium text-muted-foreground hover:bg-accent hover:text-accent-foreground"
401+
className="inline-flex items-center gap-1 rounded-md border border-input bg-background px-2 py-1.5 text-[11px] font-medium text-muted-foreground hover:bg-accent hover:text-accent-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
402402
title={`Add ${label}`}
403+
aria-label={`Add ${label} widget`}
403404
>
404405
<Plus className="h-3 w-3" />
405406
<Icon className="h-3 w-3" />
@@ -421,7 +422,7 @@ export function DashboardView({ dataSource }: { dataSource?: any }) {
421422

422423
{/* ── Main area + Config Panel ─────────────────────────────── */}
423424
<div className="flex-1 overflow-hidden flex flex-col sm:flex-row relative">
424-
<div className="flex-1 min-w-0 overflow-auto p-0 sm:p-6">
425+
<div className="flex-1 min-w-0 overflow-auto p-2 sm:p-4 md:p-6">
425426
<DashboardRenderer
426427
schema={previewSchema}
427428
dataSource={dataSource}

apps/console/src/components/MetadataFormDialog.tsx

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -127,7 +127,7 @@ export function MetadataFormDialog({
127127
<div key={field.key} className="space-y-2">
128128
<Label htmlFor={inputId}>
129129
{field.label}
130-
{field.required && <span className="text-destructive ml-1">*</span>}
130+
{field.required && <span className="text-destructive ml-1" aria-hidden="true">*</span>}
131131
</Label>
132132

133133
{field.type === 'textarea' ? (
@@ -149,7 +149,8 @@ export function MetadataFormDialog({
149149
handleChange(field.key, e.target.value)
150150
}
151151
disabled={disabled}
152-
className="flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm"
152+
aria-required={field.required ? true : undefined}
153+
className="flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50"
153154
data-testid={`metadata-field-${field.key}`}
154155
>
155156
<option value="">Select...</option>

apps/console/src/components/MetadataGrid.tsx

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,7 @@ export function MetadataGrid({
5858
return (
5959
<div className="rounded-lg border bg-card" data-testid="metadata-grid">
6060
<div className="overflow-x-auto">
61-
<table className="w-full text-sm">
61+
<table className="w-full text-sm" aria-label={`${typeLabel} list`}>
6262
<thead>
6363
<tr className="border-b bg-muted/50">
6464
{columns.map((col) => (
@@ -87,11 +87,14 @@ export function MetadataGrid({
8787
data-testid={`metadata-item-${name}`}
8888
onClick={() => onItemClick?.(name)}
8989
>
90-
{columns.map((col) => (
91-
<td key={col.key} className="px-4 py-3 truncate max-w-[200px]">
92-
{String(item[col.key] ?? '—')}
93-
</td>
94-
))}
90+
{columns.map((col) => {
91+
const cellValue = String(item[col.key] ?? '—');
92+
return (
93+
<td key={col.key} className="px-4 py-3 truncate max-w-[200px]" title={cellValue}>
94+
{cellValue}
95+
</td>
96+
);
97+
})}
9598
{editable && (
9699
<td className="px-4 py-3 text-right">
97100
<div className="flex items-center justify-end gap-1">

apps/console/src/components/SearchResultsPage.tsx

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -137,8 +137,10 @@ export function SearchResultsPage() {
137137

138138
{/* Search input */}
139139
<div className="relative">
140-
<Search className="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground" />
140+
<label htmlFor="search-results-input" className="sr-only">Search objects, dashboards, pages, reports</label>
141+
<Search className="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground" aria-hidden="true" />
141142
<Input
143+
id="search-results-input"
142144
value={query}
143145
onChange={(e: any) => handleSearch(e.target.value)}
144146
placeholder="Search objects, dashboards, pages, reports..."
@@ -180,7 +182,7 @@ export function SearchResultsPage() {
180182
{items.map(item => {
181183
const ItemIcon = TYPE_ICONS[item.type] || Database;
182184
return (
183-
<Link key={item.id} to={item.href}>
185+
<Link key={item.id} to={item.href} className="rounded-lg focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2">
184186
<Card className="hover:bg-accent/50 transition-colors cursor-pointer">
185187
<CardContent className="flex items-center gap-3 p-3">
186188
<div className={`flex h-8 w-8 items-center justify-center rounded ${TYPE_COLORS[item.type] || ''}`}>

apps/console/src/pages/home/AppCard.tsx

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
*/
88

99
import { Star, StarOff } from 'lucide-react';
10-
import { Card, CardContent, Button } from '@object-ui/components';
10+
import { Card, CardContent, Button, Badge } from '@object-ui/components';
1111
import { useObjectTranslation } from '@object-ui/i18n';
1212
import { resolveI18nLabel } from '../../utils';
1313
import { useFavorites } from '../../hooks/useFavorites';
@@ -50,8 +50,10 @@ export function AppCard({ app, onClick, isFavorite }: AppCardProps) {
5050
<Button
5151
variant="ghost"
5252
size="sm"
53-
className="absolute top-2 right-2 opacity-0 group-hover:opacity-100 transition-opacity"
53+
className="absolute top-2 right-2 opacity-0 group-hover:opacity-100 focus-visible:opacity-100 transition-opacity"
5454
onClick={handleToggleFavorite}
55+
aria-label={isFavorite ? `Remove ${label} from favorites` : `Add ${label} to favorites`}
56+
aria-pressed={isFavorite}
5557
data-testid={`favorite-btn-${app.name}`}
5658
>
5759
{isFavorite ? (
@@ -88,9 +90,9 @@ export function AppCard({ app, onClick, isFavorite }: AppCardProps) {
8890
{/* App Badge (if default) */}
8991
{app.isDefault && (
9092
<div className="mt-3">
91-
<span className="inline-flex items-center rounded-full bg-blue-50 dark:bg-blue-950 px-2 py-1 text-xs font-medium text-blue-700 dark:text-blue-300">
93+
<Badge variant="secondary">
9294
{t('home.appCard.default', { defaultValue: 'Default' })}
93-
</span>
95+
</Badge>
9496
</div>
9597
)}
9698
</CardContent>

apps/console/src/pages/home/HomePage.tsx

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -90,17 +90,17 @@ export function HomePage() {
9090
return (
9191
<div className="bg-background">
9292
{/* Page Title */}
93-
<div className="px-6 pt-6 pb-4">
94-
<h1 className="text-3xl font-bold tracking-tight">
93+
<div className="px-4 sm:px-6 pt-6 pb-4">
94+
<h1 className="text-2xl sm:text-3xl font-bold tracking-tight">
9595
{t('home.title', { defaultValue: 'Home' })}
9696
</h1>
97-
<p className="text-muted-foreground mt-1">
97+
<p className="text-sm text-muted-foreground mt-1">
9898
{t('home.subtitle', { defaultValue: 'Your workspace dashboard' })}
9999
</p>
100100
</div>
101101

102102
{/* Main Content */}
103-
<div className="px-6 py-4 space-y-8">
103+
<div className="px-4 sm:px-6 py-4 space-y-8">
104104
{/* Quick Actions */}
105105
<QuickActions />
106106

apps/console/src/pages/home/QuickActions.tsx

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,15 @@ export function QuickActions() {
6767
className="cursor-pointer hover:shadow-md transition-shadow"
6868
onClick={() => navigate(action.href)}
6969
data-testid={`quick-action-${action.id}`}
70+
role="link"
71+
tabIndex={0}
72+
onKeyDown={(e: React.KeyboardEvent) => {
73+
if (e.key === 'Enter' || e.key === ' ') {
74+
e.preventDefault();
75+
navigate(action.href);
76+
}
77+
}}
78+
aria-label={action.label}
7079
>
7180
<CardContent className="p-6">
7281
<div className="flex items-start gap-4">

apps/console/src/pages/home/RecentApps.tsx

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import { useObjectTranslation } from '@object-ui/i18n';
1111
import { Card, CardContent } from '@object-ui/components';
1212
import { Clock } from 'lucide-react';
1313
import { getIcon } from '../../utils/getIcon';
14+
import { capitalizeFirst } from '../../utils';
1415
import type { RecentItem } from '../../hooks/useRecentItems';
1516

1617
interface RecentAppsProps {
@@ -31,7 +32,7 @@ export function RecentApps({ items }: RecentAppsProps) {
3132
{t('home.recentApps.title', { defaultValue: 'Recently Accessed' })}
3233
</h2>
3334
</div>
34-
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
35+
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-4">
3536
{items.map((item) => {
3637
const Icon = getIcon(item.type);
3738
return (
@@ -40,15 +41,23 @@ export function RecentApps({ items }: RecentAppsProps) {
4041
className="cursor-pointer hover:shadow-md transition-shadow"
4142
onClick={() => navigate(item.href)}
4243
data-testid={`recent-item-${item.id}`}
44+
role="link"
45+
tabIndex={0}
46+
onKeyDown={(e: React.KeyboardEvent) => {
47+
if (e.key === 'Enter' || e.key === ' ') {
48+
e.preventDefault();
49+
navigate(item.href);
50+
}
51+
}}
4352
>
4453
<CardContent className="p-4">
4554
<div className="flex items-center gap-3">
46-
<div className="p-2 rounded-lg bg-muted">
55+
<div className="p-2 rounded-lg bg-muted shrink-0">
4756
<Icon className="h-5 w-5" />
4857
</div>
4958
<div className="flex-1 min-w-0">
5059
<h3 className="font-medium text-sm truncate">{item.label}</h3>
51-
<p className="text-xs text-muted-foreground capitalize">{item.type}</p>
60+
<p className="text-xs text-muted-foreground">{capitalizeFirst(item.type)}</p>
5261
</div>
5362
</div>
5463
</CardContent>

0 commit comments

Comments
 (0)