diff --git a/docs/filters-collapse-description.md b/docs/filters-collapse-description.md new file mode 100644 index 0000000..6919da4 --- /dev/null +++ b/docs/filters-collapse-description.md @@ -0,0 +1,88 @@ +# Polish FiltersSidebar with Collapse Persistence + +## Summary + +This PR implements collapsible filter sections in `FiltersSidebar` with localStorage persistence to remember collapsed states across page visits. Users can now collapse/expand each filter group (Categories, Price range, Popularity) independently, and their preferences are saved for future sessions. + +## Changes + +### `src/components/FiltersSidebar.tsx` +- Refactored filter sections into a reusable `FilterGroup` component with collapsible behavior +- Each filter group now has an expandable/collapsible header with a chevron indicator +- Uses `usePersistedState` hook to persist collapsed state in localStorage +- Implements proper ARIA attributes (`aria-expanded`, `aria-controls`) for accessibility +- Uses `hidden` attribute to hide collapsed content from screen readers + +### `src/components/icons/ChevronIcon.tsx` (new) +- New icon component for the expand/collapse chevron indicator +- Follows the existing icon pattern in the codebase (size variants, accessibility) + +### `src/components/icons/index.tsx` +- Added export for the new `ChevronIcon` component + +### `src/index.css` +- Added styles for `.filter-group__header`, `.filter-group__chevron`, and `.filter-group__panel` +- Includes hover and focus-visible states matching design-token patterns +- Reduced motion support via `prefers-reduced-motion` media query +- Responsive adjustments for smaller viewports (max-width: 640px) + +### `src/state/uiPrefs.ts` +- Added `isSectionCollapsed()`, `toggleSectionCollapsed()`, and `setSectionCollapsed()` functions for collapsed section state management + +### Test Files +- `src/components/FiltersSidebar.test.tsx` - New comprehensive tests for collapse functionality +- `src/state/uiPrefs.test.ts` - Tests for the uiPrefs collapse state functions + +## Accessibility (WCAG 2.1 AA) + +- `aria-expanded` indicates the current state of each filter group +- `aria-controls` associates headers with their content panels +- `hidden` attribute removes collapsed content from assistive technology +- Focus styles use `var(--accent)` with proper contrast against both light and dark backgrounds +- `prefers-reduced-motion` support for users who request reduced motion + +## Responsive Design + +The filter headers adapt to smaller screens: +- Reduced padding (10px 14px) on screens under 640px +- Smaller font size (0.9rem) on mobile viewports +- All breakpoints maintain proper touch target sizes (minimum 44px height) + +## API/Visible Changes + +### Component API (no breaking changes) +`FiltersSidebar` maintains the same external props interface - this is a purely visual enhancement. + +### localStorage Keys +- `callora.filters.categories.collapsed` - Categories section state (boolean) +- `callora.filters.price.collapsed` - Price range section state (boolean) +- `callora.filters.popularity.collapsed` - Popularity section state (boolean) + +## Test Output + +``` +✓ src/components/FiltersSidebar.test.tsx (16 tests) 966ms +✓ src/state/uiPrefs.test.ts (9 tests) 11ms +``` + +All tests pass including: +- Default expanded state verification +- localStorage persistence on toggle +- State restoration on re-render +- aria-expanded and aria-controls attribute correctness +- Independent collapse/expand of each section +- Chevron rotation animation +- Price validation error handling + +## Notes + +The existing tests in the repository have pre-existing issues unrelated to this change: +- `FiltersBottomSheet.test.tsx` - missing `window.matchMedia` mock in jsdom +- `Tabs.test.tsx` - same matchMedia issue +- `ApiDetailPage.tsx`, `ApiUsage.tsx`, `ApiCard.tsx` - JSX syntax errors in existing code + +These do not affect the functionality of the changes in this PR. + +PR: https://github.com/CalloraOrg/Callora-Frontend/pull/337 + +closes #254 \ No newline at end of file diff --git a/docs/snapshot-url-description.md b/docs/snapshot-url-description.md new file mode 100644 index 0000000..ce3b0aa --- /dev/null +++ b/docs/snapshot-url-description.md @@ -0,0 +1,66 @@ +# Add 'Snapshot endpoint' export to share UI state + +## Summary + +This PR implements a shareable URL feature that captures the current endpoint selection and parameters. Users can generate a URL snapshot of their configured endpoint with parameters and share it with others, who will see the same endpoint and parameters pre-loaded. + +## Changes + +### `src/utils/snapshotUrl.ts` (new) +- `generateSnapshotUrl(basePath, snapshot)` - Creates a URL with endpoint ID and base64-encoded params +- `parseSnapshotUrl(search)` - Parses URL to extract endpoint and params, returns null if invalid +- `copySnapshotUrl(basePath, snapshot)` - Copies snapshot URL to clipboard for sharing + +### `src/ApiUsage.tsx` +- Added `snapshotted` state to track copy feedback +- Added `handleShareSnapshot()` function to generate and copy snapshot URL +- Added `useEffect` to restore endpoint params from snapshot URL on mount +- Added "Share Snapshot" button next to "Make Test Call" button +- Imported `LinkIcon` for the share button + +### `src/components/icons/LinkIcon.tsx` (new) +- New link/chain icon component for sharing functionality +- Follows existing icon patterns (size variants, accessibility) + +### `src/components/icons/index.tsx` +- Added export for `LinkIcon` + +### `src/index.css` +- Added `.share-snapshot-button` styles with proper spacing + +### `src/utils/snapshotUrl.test.ts` (new) +- 11 tests covering URL generation, parsing, and clipboard copying +- Tests edge cases like special characters, malformed params, empty values + +## API/Visible Changes + +### URL Parameters +- `endpoint` - The selected endpoint ID (e.g., "endpoint-1", "endpoint-2") +- `params` - Base64-encoded JSON of request parameters + +### Example URL +``` +/usage?endpoint=endpoint-2¶ms=eyJhYm91dCI6MTAwLCJjdXJrenVjdXIiOiJVU0QifQ%3D%3D +``` + +## Accessibility (WCAG 2.1 AA) +- Share button has `aria-label` for screen readers +- Icon is marked `aria-hidden` when button has accessible label +- Focus styles follow existing design tokens + +## Test Output + +``` +✓ src/utils/snapshotUrl.test.ts (11 tests) 36ms +``` + +All tests pass including: +- URL generation with/without params +- Base64 encoding/decoding with Unicode support +- Graceful handling of malformed URLs +- Clipboard copy success/failure + +closes #252 + +## PR Link +https://github.com/CalloraOrg/Callora-Frontend/pull/339 \ No newline at end of file diff --git a/src/ApiUsage.tsx b/src/ApiUsage.tsx index 769ffff..0432ab2 100644 --- a/src/ApiUsage.tsx +++ b/src/ApiUsage.tsx @@ -5,6 +5,8 @@ import { formatPrice } from './utils/format'; import RequestBodyEditor from './components/RequestBodyEditor'; import type { JsonSchema } from './components/RequestBodyEditor'; import CallHistoryRow from './components/CallHistoryRow'; +import { generateSnapshotUrl, parseSnapshotUrl, copySnapshotUrl } from './utils/snapshotUrl'; +import { LinkIcon } from './components/icons'; type ApiEndpoint = { id: string; @@ -201,6 +203,38 @@ export default function ApiUsage() { const [selectedLanguage, setSelectedLanguage] = useState<'javascript' | 'python' | 'curl'>('javascript'); const [selectedRange, setSelectedRange] = useState({ preset: '24h' }); const [expandedCall, setExpandedCall] = useState(null); + const [snapshotted, setSnapshotted] = useState(false); + + // Restore endpoint params from snapshot URL on mount + useEffect(() => { + const snapshot = parseSnapshotUrl(window.location.search); + if (snapshot?.endpointId) { + const endpoint = MOCK_ENDPOINTS.find(ep => ep.id === snapshot.endpointId); + if (endpoint) { + setSelectedEndpoint(endpoint); + if (snapshot.params) { + setRequestParams(JSON.stringify(snapshot.params, null, 2)); + } + } + } + }, []); + + const handleShareSnapshot = async () => { + let parsedParams: Record | null = null; + try { + parsedParams = JSON.parse(requestParams); + } catch { + // Invalid JSON, use null + } + const success = await copySnapshotUrl(window.location.pathname, { + endpointId: selectedEndpoint.id, + params: parsedParams, + }); + if (success) { + setSnapshotted(true); + setTimeout(() => setSnapshotted(false), 2000); + } + }; // Filter call history based on selected date range const filterCallsByRange = (calls: CallRecord[]): CallRecord[] => { @@ -507,6 +541,16 @@ export default function ApiUsage() { {isLoading &&