Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
70 changes: 70 additions & 0 deletions docs/UsageChart-responsive-srcset.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
# UsageChart Responsive Image Implementation

## Summary
Added responsive image srcset support to the new UsageChart component to optimize mobile device performance by preventing download of desktop-sized assets.

## Changes Made

### New Component: UsageChart
- **File**: `src/components/UsageChart.tsx`
- **Purpose**: Display usage statistics with responsive images using srcset
- **Features**:
- Responsive image loading with three breakpoints:
- Small (≤480px): `/images/usage-chart-sm.svg`
- Medium (≤960px): `/images/usage-chart-md.svg`
- Large (≥961px): `/images/usage-chart-lg.svg`
- Lazy loading for performance
- Accessible with proper ARIA labels
- Follows the same pattern as existing PlanNudge component

### Integration
- **File**: `src/pages/ApiUsage.tsx`
- Replaced CSS-based bar chart with UsageChart component in the "Calls Over Time" section
- Maintains existing layout and styling

### Tests
- **File**: `src/components/UsageChart.test.tsx`
- **Coverage**:
- Default and custom props rendering
- Responsive srcset verification
- Fallback img attributes
- CSS class application
- Accessibility attributes

## API Changes

### New Component Props
```typescript
export interface UsageChartProps {
label?: string; // Accessible label for the chart (default: "Usage Chart")
title?: string; // Chart title (default: "Usage Statistics")
alt?: string; // Alt text for the chart image (default: "Usage statistics chart showing API call trends")
}
```

### Visible Changes
- **ApiUsage Page**: The "Calls Over Time" section now displays the UsageChart component instead of CSS-based bar chart
- **Mobile Performance**: Mobile devices will download smaller image assets instead of desktop-sized versions

## Image Assets Required
The following image files need to be added to `/public/images/`:
- `usage-chart-sm.svg` - Small version for mobile (≤480px)
- `usage-chart-md.svg` - Medium version for tablets (≤960px)
- `usage-chart-lg.svg` - Large version for desktop (≥961px)

## Browser Support
Uses standard HTML5 `<picture>` element with `srcset` and `media` attributes, supported by all modern browsers.

## Performance Impact
- **Mobile**: Reduced bandwidth usage by downloading smaller images
- **Desktop**: No change - downloads appropriate large images
- **Loading**: Lazy loading implemented for deferred image loading

## Testing
Run tests with:
```bash
npm test -- UsageChart.test.tsx
```

## Part of GrantFox FWC26 (Stellar Wave)
This implementation is part of the GrantFox FWC26 campaign focused on responsive image optimization for better mobile performance.
68 changes: 68 additions & 0 deletions docs/api-usage-reduced-motion-fallback.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
# ApiUsage Reduced-Motion Fallback

## Issue
[b#004] - Add reduced-motion fallback for ApiUsage animations

## Description
This change adds `prefers-reduced-motion` fallback support for ApiUsage page animations to ensure users who prefer reduced motion experience a static UI without pulsing, spinning, or shimmering effects.

## Changes Made

### CSS Changes (`src/index.css`)
Extended the existing `@media (prefers-reduced-motion: reduce)` block for the ApiUsage page to include the button spinner animation:

```css
@media (prefers-reduced-motion: reduce) {
.api-usage-page .status-dot {
animation: none;
opacity: 1;
}

.api-usage-page .skeleton {
animation: none;
background: var(--surface-soft);
}

.api-usage-page .button-spinner {
animation: none;
border-top-color: rgba(255, 255, 255, 0.3);
}

.api-usage-page .chart-bar {
transition: none;
}
}
```

### Test Changes
Added focused tests in both `src/pages/ApiUsage.test.tsx` and `src/ApiUsage.test.tsx` to verify that the button-spinner element has the CSS class targeted by the reduced-motion rules.

## API/Visible Changes

### User-Visible Changes
- **Users with `prefers-reduced-motion: reduce` enabled** will see:
- Static status dots (no pulsing animation)
- Static skeleton loaders (no shimmer animation)
- Static button spinners (no rotation animation)
- Instant chart bar transitions (no animation delay)
- **Users without reduced-motion preference** will see no change in behavior

### API Changes
- No API changes
- No breaking changes
- No new props or component interfaces

## Accessibility Impact
This change improves accessibility for users with vestibular disorders or motion sensitivity by providing a static fallback when the OS-level reduced-motion preference is enabled. This follows WCAG 2.1 guidelines for avoiding motion that can cause discomfort or nausea.

## Testing
Added test cases to verify:
1. Button spinner CSS class is present and targeted by reduced-motion rules
2. Existing reduced-motion behavior for status dots, skeletons, and chart bars remains unchanged

## Browser Support
Uses standard CSS `@media (prefers-reduced-motion: reduce)` query, supported by all modern browsers:
- Chrome 74+
- Firefox 63+
- Safari 10.1+
- Edge 79+
22 changes: 22 additions & 0 deletions src/ApiUsage.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@

it('should enable reset button when filters are active and announce reset to screen readers', async () => {
render(<ApiUsage />);
const resetButton = screen.getByRole('button', { name: /Reset Filters/i });

Check failure on line 49 in src/ApiUsage.test.tsx

View workflow job for this annotation

GitHub Actions / Unit tests (Vitest)

src/ApiUsage.test.tsx > ApiUsage - Filter Reset > should enable reset button when filters are active and announce reset to screen readers

TestingLibraryElementError: Unable to find an accessible element with the role "button" and name `/Reset Filters/i` There are no accessible roles. But there might be some inaccessible roles. If you wish to access them, then set the `hidden` option to `true`. Learn more about this here: https://testing-library.com/docs/dom-testing-library/api-queries#byrole Ignored nodes: comments, script, style <body> <div> <div aria-busy="true" aria-label="API usage loading shell" class="api-usage-page" > <div aria-hidden="true" style="display: flex; gap: 8px; margin-bottom: 24px;" > <div aria-hidden="true" class="skeleton skeleton--stellar" role="presentation" style="width: 80px; height: 16px;" /> <span style="color: var(--text-secondary);" > / </span> <div aria-hidden="true" class="skeleton skeleton--stellar" role="presentation" style="width: 160px; height: 16px;" /> </div> <div aria-hidden="true" class="api-header" style="margin-bottom: 24px;" > <div class="api-header-info" style="display: flex; gap: 16px; align-items: center;" > <div class="api-logo" > <div aria-hidden="true" class="skeleton skeleton--stellar" role="presentation" style="width: 56px; height: 56px; border-radius: 12px;" /> </div> <div style="display: grid; gap: 8px; flex: 1 1 0%;" > <div aria-hidden="true" class="skeleton skeleton--stellar" role="presentation" style="width: 40%; height: 28px;" /> <div aria-hidden="true" class="skeleton skeleton--stellar" role="presentation" style="width: 60%; height: 16px;" /> </div> </div> <div class="api-header-actions" style="display: flex; gap: 12px;" > <div aria-hidden="true" class="skeleton skeleton--stellar" role="presentation" style="width: 150px; height: 36px; border-radius: 8px;" /> <div aria-hidden="true" class="skeleton skeleton--stellar" role="presentation" style="width: 90px; height: 36px; border-radius: 8px;" /> <div aria-hidden="true" class="skeleton skeleton--stellar" role="presentation" style="width: 110px; height: 36px; border-radius: 8px;" /> </div> </div> <div aria-hidden="true" class="surface api-key-section" style="margin-bottom: 24px;" > <div aria-hidden="true" class="skeleton skeleton--stellar" role="presentation" style="width: 100px; height: 20px; margin-bottom: 16px;" /> <div class="api-key-card" style="padding: 20px;" > <div class="api-key-display" style="display: flex; gap: 12px; align-items: center;" > <div class="key-input-group" style="flex: 1 1 0%;" > <div aria-hidden="true" class="skeleton skeleton--stellar" role="presentation" style="width: 100%; height: 38px; border-radius: 8px;" /> </div> <div class="key-actions" style="display: flex; gap: 12px;" > <div aria-hidden="true" class="skeleton skeleton--stellar" role="presentation" style="width: 80px; height: 38px; border-radius: 8px;"

Check failure on line 49 in src/ApiUsage.test.tsx

View workflow job for this annotation

GitHub Actions / Test coverage

src/ApiUsage.test.tsx > ApiUsage - Filter Reset > should enable reset button when filters are active and announce reset to screen readers

TestingLibraryElementError: Unable to find an accessible element with the role "button" and name `/Reset Filters/i` There are no accessible roles. But there might be some inaccessible roles. If you wish to access them, then set the `hidden` option to `true`. Learn more about this here: https://testing-library.com/docs/dom-testing-library/api-queries#byrole Ignored nodes: comments, script, style <body> <div> <div aria-busy="true" aria-label="API usage loading shell" class="api-usage-page" > <div aria-hidden="true" style="display: flex; gap: 8px; margin-bottom: 24px;" > <div aria-hidden="true" class="skeleton skeleton--stellar" role="presentation" style="width: 80px; height: 16px;" /> <span style="color: var(--text-secondary);" > / </span> <div aria-hidden="true" class="skeleton skeleton--stellar" role="presentation" style="width: 160px; height: 16px;" /> </div> <div aria-hidden="true" class="api-header" style="margin-bottom: 24px;" > <div class="api-header-info" style="display: flex; gap: 16px; align-items: center;" > <div class="api-logo" > <div aria-hidden="true" class="skeleton skeleton--stellar" role="presentation" style="width: 56px; height: 56px; border-radius: 12px;" /> </div> <div style="display: grid; gap: 8px; flex: 1 1 0%;" > <div aria-hidden="true" class="skeleton skeleton--stellar" role="presentation" style="width: 40%; height: 28px;" /> <div aria-hidden="true" class="skeleton skeleton--stellar" role="presentation" style="width: 60%; height: 16px;" /> </div> </div> <div class="api-header-actions" style="display: flex; gap: 12px;" > <div aria-hidden="true" class="skeleton skeleton--stellar" role="presentation" style="width: 150px; height: 36px; border-radius: 8px;" /> <div aria-hidden="true" class="skeleton skeleton--stellar" role="presentation" style="width: 90px; height: 36px; border-radius: 8px;" /> <div aria-hidden="true" class="skeleton skeleton--stellar" role="presentation" style="width: 110px; height: 36px; border-radius: 8px;" /> </div> </div> <div aria-hidden="true" class="surface api-key-section" style="margin-bottom: 24px;" > <div aria-hidden="true" class="skeleton skeleton--stellar" role="presentation" style="width: 100px; height: 20px; margin-bottom: 16px;" /> <div class="api-key-card" style="padding: 20px;" > <div class="api-key-display" style="display: flex; gap: 12px; align-items: center;" > <div class="key-input-group" style="flex: 1 1 0%;" > <div aria-hidden="true" class="skeleton skeleton--stellar" role="presentation" style="width: 100%; height: 38px; border-radius: 8px;" /> </div> <div class="key-actions" style="display: flex; gap: 12px;" > <div aria-hidden="true" class="skeleton skeleton--stellar" role="presentation" style="width: 80px; height: 38px; border-radius: 8px;"
expect(resetButton.disabled).toBe(true);
const successTab = screen.getByRole('tab', { name: /Success/i });
fireEvent.click(successTab);
Expand All @@ -59,7 +59,7 @@

it('renders an accessible breadcrumb with the current page announced', () => {
render(<ApiUsage />);
const breadcrumb = screen.getByRole('navigation', { name: /breadcrumb/i });

Check failure on line 62 in src/ApiUsage.test.tsx

View workflow job for this annotation

GitHub Actions / Unit tests (Vitest)

src/ApiUsage.test.tsx > ApiUsage - Filter Reset > renders an accessible breadcrumb with the current page announced

TestingLibraryElementError: Unable to find an accessible element with the role "navigation" and name `/breadcrumb/i` There are no accessible roles. But there might be some inaccessible roles. If you wish to access them, then set the `hidden` option to `true`. Learn more about this here: https://testing-library.com/docs/dom-testing-library/api-queries#byrole Ignored nodes: comments, script, style <body> <div> <div aria-busy="true" aria-label="API usage loading shell" class="api-usage-page" > <div aria-hidden="true" style="display: flex; gap: 8px; margin-bottom: 24px;" > <div aria-hidden="true" class="skeleton skeleton--stellar" role="presentation" style="width: 80px; height: 16px;" /> <span style="color: var(--text-secondary);" > / </span> <div aria-hidden="true" class="skeleton skeleton--stellar" role="presentation" style="width: 160px; height: 16px;" /> </div> <div aria-hidden="true" class="api-header" style="margin-bottom: 24px;" > <div class="api-header-info" style="display: flex; gap: 16px; align-items: center;" > <div class="api-logo" > <div aria-hidden="true" class="skeleton skeleton--stellar" role="presentation" style="width: 56px; height: 56px; border-radius: 12px;" /> </div> <div style="display: grid; gap: 8px; flex: 1 1 0%;" > <div aria-hidden="true" class="skeleton skeleton--stellar" role="presentation" style="width: 40%; height: 28px;" /> <div aria-hidden="true" class="skeleton skeleton--stellar" role="presentation" style="width: 60%; height: 16px;" /> </div> </div> <div class="api-header-actions" style="display: flex; gap: 12px;" > <div aria-hidden="true" class="skeleton skeleton--stellar" role="presentation" style="width: 150px; height: 36px; border-radius: 8px;" /> <div aria-hidden="true" class="skeleton skeleton--stellar" role="presentation" style="width: 90px; height: 36px; border-radius: 8px;" /> <div aria-hidden="true" class="skeleton skeleton--stellar" role="presentation" style="width: 110px; height: 36px; border-radius: 8px;" /> </div> </div> <div aria-hidden="true" class="surface api-key-section" style="margin-bottom: 24px;" > <div aria-hidden="true" class="skeleton skeleton--stellar" role="presentation" style="width: 100px; height: 20px; margin-bottom: 16px;" /> <div class="api-key-card" style="padding: 20px;" > <div class="api-key-display" style="display: flex; gap: 12px; align-items: center;" > <div class="key-input-group" style="flex: 1 1 0%;" > <div aria-hidden="true" class="skeleton skeleton--stellar" role="presentation" style="width: 100%; height: 38px; border-radius: 8px;" /> </div> <div class="key-actions" style="display: flex; gap: 12px;" > <div aria-hidden="true" class="skeleton skeleton--stellar" role="presentation" style="width: 80px; height: 38px; border-radius: 8px;"

Check failure on line 62 in src/ApiUsage.test.tsx

View workflow job for this annotation

GitHub Actions / Test coverage

src/ApiUsage.test.tsx > ApiUsage - Filter Reset > renders an accessible breadcrumb with the current page announced

TestingLibraryElementError: Unable to find an accessible element with the role "navigation" and name `/breadcrumb/i` There are no accessible roles. But there might be some inaccessible roles. If you wish to access them, then set the `hidden` option to `true`. Learn more about this here: https://testing-library.com/docs/dom-testing-library/api-queries#byrole Ignored nodes: comments, script, style <body> <div> <div aria-busy="true" aria-label="API usage loading shell" class="api-usage-page" > <div aria-hidden="true" style="display: flex; gap: 8px; margin-bottom: 24px;" > <div aria-hidden="true" class="skeleton skeleton--stellar" role="presentation" style="width: 80px; height: 16px;" /> <span style="color: var(--text-secondary);" > / </span> <div aria-hidden="true" class="skeleton skeleton--stellar" role="presentation" style="width: 160px; height: 16px;" /> </div> <div aria-hidden="true" class="api-header" style="margin-bottom: 24px;" > <div class="api-header-info" style="display: flex; gap: 16px; align-items: center;" > <div class="api-logo" > <div aria-hidden="true" class="skeleton skeleton--stellar" role="presentation" style="width: 56px; height: 56px; border-radius: 12px;" /> </div> <div style="display: grid; gap: 8px; flex: 1 1 0%;" > <div aria-hidden="true" class="skeleton skeleton--stellar" role="presentation" style="width: 40%; height: 28px;" /> <div aria-hidden="true" class="skeleton skeleton--stellar" role="presentation" style="width: 60%; height: 16px;" /> </div> </div> <div class="api-header-actions" style="display: flex; gap: 12px;" > <div aria-hidden="true" class="skeleton skeleton--stellar" role="presentation" style="width: 150px; height: 36px; border-radius: 8px;" /> <div aria-hidden="true" class="skeleton skeleton--stellar" role="presentation" style="width: 90px; height: 36px; border-radius: 8px;" /> <div aria-hidden="true" class="skeleton skeleton--stellar" role="presentation" style="width: 110px; height: 36px; border-radius: 8px;" /> </div> </div> <div aria-hidden="true" class="surface api-key-section" style="margin-bottom: 24px;" > <div aria-hidden="true" class="skeleton skeleton--stellar" role="presentation" style="width: 100px; height: 20px; margin-bottom: 16px;" /> <div class="api-key-card" style="padding: 20px;" > <div class="api-key-display" style="display: flex; gap: 12px; align-items: center;" > <div class="key-input-group" style="flex: 1 1 0%;" > <div aria-hidden="true" class="skeleton skeleton--stellar" role="presentation" style="width: 100%; height: 38px; border-radius: 8px;" /> </div> <div class="key-actions" style="display: flex; gap: 12px;" > <div aria-hidden="true" class="skeleton skeleton--stellar" role="presentation" style="width: 80px; height: 38px; border-radius: 8px;"
expect(breadcrumb).toBeTruthy();
const marketplaceLink = screen.getByRole('link', { name: 'Marketplace' });
expect(marketplaceLink.getAttribute('href')).toBe('/marketplace');
Expand Down Expand Up @@ -89,7 +89,7 @@
it('applies tabular-nums to all stat-value elements', () => {
render(<ApiUsage />);
const statValues = document.querySelectorAll('.stat-value.tabular-nums');
expect(statValues.length).toBe(5);

Check failure on line 92 in src/ApiUsage.test.tsx

View workflow job for this annotation

GitHub Actions / Unit tests (Vitest)

src/ApiUsage.test.tsx > ApiUsage - Tabular Numerals > applies tabular-nums to all stat-value elements

AssertionError: expected +0 to be 5 // Object.is equality - Expected + Received - 5 + 0 ❯ src/ApiUsage.test.tsx:92:31

Check failure on line 92 in src/ApiUsage.test.tsx

View workflow job for this annotation

GitHub Actions / Test coverage

src/ApiUsage.test.tsx > ApiUsage - Tabular Numerals > applies tabular-nums to all stat-value elements

AssertionError: expected +0 to be 5 // Object.is equality - Expected + Received - 5 + 0 ❯ src/ApiUsage.test.tsx:92:31
const labels = ['Calls Today', 'Calls This Week', 'Total Spent', 'Avg Response Time', 'Success Rate'];
statValues.forEach((el, i) => {
expect(el.classList.contains('tabular-nums')).toBe(true);
Expand Down Expand Up @@ -158,6 +158,28 @@

expect(screen.getByText('Call History')).toBeTruthy();
});

it('button-spinner has CSS class targeted by prefers-reduced-motion: reduce rules', () => {
window.matchMedia = vi.fn().mockImplementation((query) => ({
matches: false,
media: query,
onchange: null,
addListener: vi.fn(),
removeListener: vi.fn(),
addEventListener: vi.fn(),
removeEventListener: vi.fn(),
dispatchEvent: vi.fn(),
}));

render(<ApiUsage />);

// Trigger button loading state to show spinner
const makeTestCallButton = screen.getByRole('button', { name: /Make Test Call/i });

Check failure on line 177 in src/ApiUsage.test.tsx

View workflow job for this annotation

GitHub Actions / Unit tests (Vitest)

src/ApiUsage.test.tsx > ApiUsage - prefers-reduced-motion > button-spinner has CSS class targeted by prefers-reduced-motion: reduce rules

TestingLibraryElementError: Unable to find an accessible element with the role "button" and name `/Make Test Call/i` There are no accessible roles. But there might be some inaccessible roles. If you wish to access them, then set the `hidden` option to `true`. Learn more about this here: https://testing-library.com/docs/dom-testing-library/api-queries#byrole Ignored nodes: comments, script, style <body> <div> <div aria-busy="true" aria-label="API usage loading shell" class="api-usage-page" > <div aria-hidden="true" style="display: flex; gap: 8px; margin-bottom: 24px;" > <div aria-hidden="true" class="skeleton skeleton--stellar" role="presentation" style="width: 80px; height: 16px;" /> <span style="color: var(--text-secondary);" > / </span> <div aria-hidden="true" class="skeleton skeleton--stellar" role="presentation" style="width: 160px; height: 16px;" /> </div> <div aria-hidden="true" class="api-header" style="margin-bottom: 24px;" > <div class="api-header-info" style="display: flex; gap: 16px; align-items: center;" > <div class="api-logo" > <div aria-hidden="true" class="skeleton skeleton--stellar" role="presentation" style="width: 56px; height: 56px; border-radius: 12px;" /> </div> <div style="display: grid; gap: 8px; flex: 1 1 0%;" > <div aria-hidden="true" class="skeleton skeleton--stellar" role="presentation" style="width: 40%; height: 28px;" /> <div aria-hidden="true" class="skeleton skeleton--stellar" role="presentation" style="width: 60%; height: 16px;" /> </div> </div> <div class="api-header-actions" style="display: flex; gap: 12px;" > <div aria-hidden="true" class="skeleton skeleton--stellar" role="presentation" style="width: 150px; height: 36px; border-radius: 8px;" /> <div aria-hidden="true" class="skeleton skeleton--stellar" role="presentation" style="width: 90px; height: 36px; border-radius: 8px;" /> <div aria-hidden="true" class="skeleton skeleton--stellar" role="presentation" style="width: 110px; height: 36px; border-radius: 8px;" /> </div> </div> <div aria-hidden="true" class="surface api-key-section" style="margin-bottom: 24px;" > <div aria-hidden="true" class="skeleton skeleton--stellar" role="presentation" style="width: 100px; height: 20px; margin-bottom: 16px;" /> <div class="api-key-card" style="padding: 20px;" > <div class="api-key-display" style="display: flex; gap: 12px; align-items: center;" > <div class="key-input-group" style="flex: 1 1 0%;" > <div aria-hidden="true" class="skeleton skeleton--stellar" role="presentation" style="width: 100%; height: 38px; border-radius: 8px;" /> </div> <div class="key-actions" style="display: flex; gap: 12px;" > <div aria-hidden="true" class="skeleton skeleton--stellar" role="presentation" style="width: 80px; height: 38px; border-radius: 8px;"

Check failure on line 177 in src/ApiUsage.test.tsx

View workflow job for this annotation

GitHub Actions / Test coverage

src/ApiUsage.test.tsx > ApiUsage - prefers-reduced-motion > button-spinner has CSS class targeted by prefers-reduced-motion: reduce rules

TestingLibraryElementError: Unable to find an accessible element with the role "button" and name `/Make Test Call/i` There are no accessible roles. But there might be some inaccessible roles. If you wish to access them, then set the `hidden` option to `true`. Learn more about this here: https://testing-library.com/docs/dom-testing-library/api-queries#byrole Ignored nodes: comments, script, style <body> <div> <div aria-busy="true" aria-label="API usage loading shell" class="api-usage-page" > <div aria-hidden="true" style="display: flex; gap: 8px; margin-bottom: 24px;" > <div aria-hidden="true" class="skeleton skeleton--stellar" role="presentation" style="width: 80px; height: 16px;" /> <span style="color: var(--text-secondary);" > / </span> <div aria-hidden="true" class="skeleton skeleton--stellar" role="presentation" style="width: 160px; height: 16px;" /> </div> <div aria-hidden="true" class="api-header" style="margin-bottom: 24px;" > <div class="api-header-info" style="display: flex; gap: 16px; align-items: center;" > <div class="api-logo" > <div aria-hidden="true" class="skeleton skeleton--stellar" role="presentation" style="width: 56px; height: 56px; border-radius: 12px;" /> </div> <div style="display: grid; gap: 8px; flex: 1 1 0%;" > <div aria-hidden="true" class="skeleton skeleton--stellar" role="presentation" style="width: 40%; height: 28px;" /> <div aria-hidden="true" class="skeleton skeleton--stellar" role="presentation" style="width: 60%; height: 16px;" /> </div> </div> <div class="api-header-actions" style="display: flex; gap: 12px;" > <div aria-hidden="true" class="skeleton skeleton--stellar" role="presentation" style="width: 150px; height: 36px; border-radius: 8px;" /> <div aria-hidden="true" class="skeleton skeleton--stellar" role="presentation" style="width: 90px; height: 36px; border-radius: 8px;" /> <div aria-hidden="true" class="skeleton skeleton--stellar" role="presentation" style="width: 110px; height: 36px; border-radius: 8px;" /> </div> </div> <div aria-hidden="true" class="surface api-key-section" style="margin-bottom: 24px;" > <div aria-hidden="true" class="skeleton skeleton--stellar" role="presentation" style="width: 100px; height: 20px; margin-bottom: 16px;" /> <div class="api-key-card" style="padding: 20px;" > <div class="api-key-display" style="display: flex; gap: 12px; align-items: center;" > <div class="key-input-group" style="flex: 1 1 0%;" > <div aria-hidden="true" class="skeleton skeleton--stellar" role="presentation" style="width: 100%; height: 38px; border-radius: 8px;" /> </div> <div class="key-actions" style="display: flex; gap: 12px;" > <div aria-hidden="true" class="skeleton skeleton--stellar" role="presentation" style="width: 80px; height: 38px; border-radius: 8px;"
fireEvent.click(makeTestCallButton);

const buttonSpinner = document.querySelector('.button-spinner');
expect(buttonSpinner).toBeTruthy();
});
});

describe('ApiUsage - Design Token Spacing (v7)', () => {
Expand Down
75 changes: 75 additions & 0 deletions src/components/UsageChart.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
import { render, screen } from '@testing-library/react';
import UsageChart from './UsageChart';

describe('UsageChart', () => {
it('renders with default props', () => {
render(<UsageChart />);

expect(screen.getByLabelText('Usage Chart')).toBeInTheDocument();
expect(screen.getByText('Usage Statistics')).toBeInTheDocument();
});

it('renders with custom props', () => {
render(
<UsageChart
label="Custom Label"
title="Custom Title"
alt="Custom alt text"
/>
);

expect(screen.getByLabelText('Custom Label')).toBeInTheDocument();
expect(screen.getByText('Custom Title')).toBeInTheDocument();
});

it('renders responsive image with srcset', () => {
render(<UsageChart />);

const picture = document.querySelector('picture');
expect(picture).toBeInTheDocument();

const sources = picture?.querySelectorAll('source');
expect(sources).toHaveLength(3);

// Check small screen source
expect(sources[0]).toHaveAttribute('srcSet', '/images/usage-chart-sm.svg');
expect(sources[0]).toHaveAttribute('media', '(max-width: 480px)');

// Check medium screen source
expect(sources[1]).toHaveAttribute('srcSet', '/images/usage-chart-md.svg');
expect(sources[1]).toHaveAttribute('media', '(max-width: 960px)');

// Check large screen source
expect(sources[2]).toHaveAttribute('srcSet', '/images/usage-chart-lg.svg');
expect(sources[2]).toHaveAttribute('media', '(min-width: 961px)');
});

it('renders fallback img with correct attributes', () => {
render(<UsageChart />);

const img = document.querySelector('img');
expect(img).toBeInTheDocument();
expect(img).toHaveAttribute('src', '/images/usage-chart-md.svg');
expect(img).toHaveAttribute('alt', 'Usage statistics chart showing API call trends');
expect(img).toHaveAttribute('loading', 'lazy');
expect(img).toHaveAttribute('width', '400');
expect(img).toHaveAttribute('height', '250');
});

it('applies correct CSS classes', () => {
render(<UsageChart />);

const container = screen.getByLabelText('Usage Chart');
expect(container).toHaveClass('usage-chart');

const img = document.querySelector('img');
expect(img).toHaveClass('usage-chart__img');
});

it('has aria-hidden on illustration container', () => {
render(<UsageChart />);

const illustration = document.querySelector('.usage-chart__illustration');
expect(illustration).toHaveAttribute('aria-hidden', 'true');
});
});
52 changes: 52 additions & 0 deletions src/components/UsageChart.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import React from 'react';

export interface UsageChartProps {
/** Accessible label for the chart */
label?: string;
/** Chart title */
title?: string;
/** Alt text for the chart image */
alt?: string;
}

/**
* UsageChart displays usage statistics with responsive images using srcset.
* Mobile devices download smaller image assets while desktop devices get larger ones.
*
* Part of GrantFox FWC26 (Stellar Wave) responsive image optimization.
*/
export default function UsageChart({
label = 'Usage Chart',
title = 'Usage Statistics',
alt = 'Usage statistics chart showing API call trends',
}: UsageChartProps) {
return (
<div className="usage-chart" aria-label={label}>
<h3 className="usage-chart__title">{title}</h3>
<div className="usage-chart__illustration" aria-hidden="true">
<picture>
<source
srcSet="/images/usage-chart-sm.svg"
media="(max-width: 480px)"
/>
<source
srcSet="/images/usage-chart-md.svg"
media="(max-width: 960px)"
/>
<source
srcSet="/images/usage-chart-lg.svg"
media="(min-width: 961px)"
/>
<img
src="/images/usage-chart-md.svg"
alt={alt}
className="usage-chart__img"
loading="lazy"
width="400"
height="250"
/>
</picture>
</div>
</div>
);
}
7 changes: 6 additions & 1 deletion src/index.css
Original file line number Diff line number Diff line change
Expand Up @@ -4292,7 +4292,7 @@ code,

/* ── ApiUsage — prefers-reduced-motion fallback (Issue #721) ───────────
When users prefer reduced motion, disable pulsing status dots, skeleton
shimmer animations, and chart-bar transitions in the ApiUsage section
shimmer animations, button spinner, and chart-bar transitions in the ApiUsage section
so the UI stays completely static. */
@media (prefers-reduced-motion: reduce) {
.api-usage-page .status-dot {
Expand All @@ -4305,6 +4305,11 @@ code,
background: var(--surface-soft);
}

.api-usage-page .button-spinner {
animation: none;
border-top-color: rgba(255, 255, 255, 0.3);
}

.api-usage-page .chart-bar {
transition: none;
}
Expand Down
12 changes: 12 additions & 0 deletions src/pages/ApiUsage.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -364,6 +364,18 @@ describe('ApiUsage - prefers-reduced-motion', () => {
expect(el.classList.contains('skeleton')).toBe(true);
});
});

it('button-spinner has CSS class targeted by prefers-reduced-motion: reduce rules', () => {
render(<ApiUsage />);
act(() => { vi.advanceTimersByTime(500); });

// Trigger button loading state to show spinner
const makeTestCallButton = screen.getByRole('button', { name: /Make Test Call/i });
fireEvent.click(makeTestCallButton);

const buttonSpinner = document.querySelector('.button-spinner');
expect(buttonSpinner).toBeTruthy();
});
});

describe('ApiUsage - Skeleton Parity', () => {
Expand Down
22 changes: 5 additions & 17 deletions src/pages/ApiUsage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import CallHistoryRow from '../components/CallHistoryRow';
import Breadcrumb from '../components/Breadcrumb';
import RequestHistoryPanel from '../components/RequestHistoryPanel';
import ParamsBuilder from '../components/ParamsBuilder';
import UsageChart from '../components/UsageChart';
import { useFetchTracker } from '../hooks/useFetchTracker';
import { useQuota } from '../hooks/useQuota';
import PlanNudge from '../components/PlanNudge';
Expand Down Expand Up @@ -747,23 +748,10 @@ export default function ApiUsage() {
<div className="mini-chart">
<h3>Calls Over Time</h3>
<CallsHeatmap />
<div className="chart-placeholder" style={{ marginTop: '24px' }}>
{/* Simple bar chart visualization */}
<div className="chart-bars">
{[65, 59, 80, 81, 56, 55, 47].map((height, i) => (
<div key={i} className="chart-bar" style={{ height: `${height}%` }}></div>
))}
</div>
<div className="chart-labels">
<span>Mon</span>
<span>Tue</span>
<span>Wed</span>
<span>Thu</span>
<span>Fri</span>
<span>Sat</span>
<span>Sun</span>
</div>
</div>
<UsageChart
title="API Call Trends"
alt="Usage statistics chart showing API call trends over time"
/>
</div>
</div>

Expand Down
Loading