('dense');
+
+ const filteredItems = useMemo(
+ () => (status === 'populated' ? applyLibraryFilters(items, filters) : []),
+ [items, filters, status],
+ );
+
+ const isFiltered =
+ filters.search.trim().length > 0 || filters.typeFilter !== 'all';
+
+ const handleClearFilters = useCallback(() => {
+ setFilters(DEFAULT_FILTERS);
+ }, []);
+
+ if (status === 'loading') {
+ return (
+
+
+
+ );
+ }
+
+ if (status === 'error') {
+ return (
+
+
+
+ );
+ }
+
+ if (status === 'empty') {
+ return (
+
+ );
+ }
+
+ return (
+
+ {storage &&
}
+
+
+
+ {filteredItems.length === 0 ? (
+
+ ) : viewMode === 'preview' ? (
+
+ {filteredItems.map((item) => (
+
+ ))}
+
+ ) : (
+
+ {filteredItems.map((item) => (
+
+ ))}
+
+ )}
+
+ );
+}
\ No newline at end of file
diff --git a/client/src/components/Library/LibraryListItem.tsx b/client/src/components/Library/LibraryListItem.tsx
new file mode 100644
index 0000000..297c98f
--- /dev/null
+++ b/client/src/components/Library/LibraryListItem.tsx
@@ -0,0 +1,80 @@
+import { FileIcon, Sparkles } from '@librechat/client';
+import type { LibraryItem } from '~/types/library';
+import { cn } from '~/utils';
+import { formatLibraryDate, formatLibraryFileSize, LIBRARY_TYPE_LABELS } from './libraryUtils';
+import { getLibraryFileType, TYPE_COLOR_MAP } from './libraryTypeMeta';
+import LibraryItemActions from './LibraryItemActions';
+
+interface LibraryListItemProps {
+ item: LibraryItem;
+ onClick?: (item: LibraryItem) => void;
+}
+
+const itemRowClassName =
+ 'flex w-full items-center gap-3 rounded-lg border border-border-light bg-surface-primary px-4 py-3 text-left transition-colors';
+
+export default function LibraryListItem({ item, onClick }: LibraryListItemProps) {
+ const fileType = getLibraryFileType(item.type);
+ const isArtifact = item.type === 'artifact';
+ const iconColor = TYPE_COLOR_MAP[item.type];
+ const ariaLabel = `${item.name}, ${LIBRARY_TYPE_LABELS[item.type]}, ${formatLibraryFileSize(item.sizeBytes)}`;
+
+ const content = (
+ <>
+
+ {isArtifact ? : }
+
+
+
+
+
{item.name}
+
+ {formatLibraryFileSize(item.sizeBytes)}
+
+
+
+
+ {LIBRARY_TYPE_LABELS[item.type]}
+
+ {item.kind}
+ ·
+
+
+ {item.description && (
+
{item.description}
+ )}
+
+ >
+ );
+
+ return (
+
+ {onClick ? (
+
+ ) : (
+
{content}
+ )}
+
+
+ );
+}
diff --git a/client/src/components/Library/LibraryListWithMockData.tsx b/client/src/components/Library/LibraryListWithMockData.tsx
new file mode 100644
index 0000000..1a08a7c
--- /dev/null
+++ b/client/src/components/Library/LibraryListWithMockData.tsx
@@ -0,0 +1,29 @@
+import LibraryList from './LibraryList';
+import { useMockLibraryData, type MockLibraryScenario } from './useMockLibraryData';
+
+interface LibraryListWithMockDataProps {
+ scenario?: MockLibraryScenario;
+ className?: string;
+}
+
+/**
+ * Convenience wrapper that wires LibraryList to the mock data hook.
+ * Drop-in component for development until the real API is integrated.
+ */
+export default function LibraryListWithMockData({
+ scenario = 'success',
+ className,
+}: LibraryListWithMockDataProps) {
+ const { items, storage, status, errorMessage, refetch } = useMockLibraryData({ scenario });
+
+ return (
+
+ );
+}
\ No newline at end of file
diff --git a/client/src/components/Library/LibraryLoadingState.tsx b/client/src/components/Library/LibraryLoadingState.tsx
new file mode 100644
index 0000000..e9d81ad
--- /dev/null
+++ b/client/src/components/Library/LibraryLoadingState.tsx
@@ -0,0 +1,28 @@
+import { Skeleton } from '@librechat/client';
+
+const SKELETON_ROWS = 6;
+
+export default function LibraryLoadingState() {
+ return (
+
+
+
+
+ {Array.from({ length: SKELETON_ROWS }).map((_, index) => (
+
+ ))}
+
+
+ );
+}
\ No newline at end of file
diff --git a/client/src/components/Library/LibraryPreviewCard.tsx b/client/src/components/Library/LibraryPreviewCard.tsx
new file mode 100644
index 0000000..45ba5ff
--- /dev/null
+++ b/client/src/components/Library/LibraryPreviewCard.tsx
@@ -0,0 +1,177 @@
+import { FileIcon, Sparkles } from '@librechat/client';
+import type { LibraryItem } from '~/types/library';
+import { cn } from '~/utils';
+import { formatLibraryDate, formatLibraryFileSize, LIBRARY_TYPE_LABELS } from './libraryUtils';
+import { getLibraryFileType, TYPE_COLOR_MAP } from './libraryTypeMeta';
+import LibraryItemActions from './LibraryItemActions';
+
+interface LibraryPreviewCardProps {
+ item: LibraryItem;
+ onClick?: (item: LibraryItem) => void;
+}
+
+function isPresentation(item: LibraryItem): boolean {
+ return item.name.toLowerCase().endsWith('.pptx') || item.mimeType?.includes('presentation') === true;
+}
+
+function isWordOrMarkdown(item: LibraryItem): boolean {
+ const lower = item.name.toLowerCase();
+ return lower.endsWith('.docx') || lower.endsWith('.md') || item.mimeType?.includes('wordprocessing') === true;
+}
+
+function DeckPreview() {
+ return (
+
+ );
+}
+
+function DocumentPreview() {
+ return (
+
+ );
+}
+
+function SpreadsheetPreview() {
+ return (
+
+
+ {Array.from({ length: 4 }).map((_, i) => (
+
+ ))}
+
+ {Array.from({ length: 4 }).map((_, row) => (
+
+ {Array.from({ length: 4 }).map((_, col) => (
+
+ ))}
+
+ ))}
+
+ );
+}
+
+function CodePreview() {
+ const bars = [
+ 'w-3/4 bg-emerald-600',
+ 'w-1/2 bg-blue-500',
+ 'w-5/6 bg-amber-500',
+ 'w-2/3 bg-emerald-600',
+ 'w-3/4 bg-blue-500',
+ ];
+ return (
+
+ {bars.map((bar, i) => (
+
+ ))}
+
+ );
+}
+
+function PreviewArea({ item }: { item: LibraryItem }) {
+ if (item.type === 'pdf' || isWordOrMarkdown(item)) {
+ return ;
+ }
+ if (item.type === 'spreadsheet') {
+ return ;
+ }
+ if (item.type === 'code' || (item.mimeType && item.mimeType.includes('html'))) {
+ return ;
+ }
+ if (isPresentation(item)) {
+ return ;
+ }
+
+ const fileType = getLibraryFileType(item.type);
+ const iconColor = TYPE_COLOR_MAP[item.type];
+ const isArtifact = item.type === 'artifact';
+
+ return (
+
+ {isArtifact ? : }
+
+ );
+}
+
+export default function LibraryPreviewCard({ item, onClick }: LibraryPreviewCardProps) {
+ const fileType = getLibraryFileType(item.type);
+ const isArtifact = item.type === 'artifact';
+ const iconColor = TYPE_COLOR_MAP[item.type];
+ const ariaLabel = `${item.name}, ${LIBRARY_TYPE_LABELS[item.type]}, ${formatLibraryFileSize(item.sizeBytes)}`;
+
+ const sourceLabel = item.conversationId ? `Chat ${item.conversationId}` : item.kind;
+
+ return (
+
+
+
+
+
+
+ {isArtifact ? : }
+
+
+ {item.name}
+
+
+
+
+
+ {LIBRARY_TYPE_LABELS[item.type]}
+
+ {sourceLabel}
+
+
+
+
+ ·
+ {formatLibraryFileSize(item.sizeBytes)}
+
+
+
+ Quick actions
+
+
+
+
+ );
+}
diff --git a/client/src/components/Library/LibraryStorageBar.tsx b/client/src/components/Library/LibraryStorageBar.tsx
new file mode 100644
index 0000000..bfa054e
--- /dev/null
+++ b/client/src/components/Library/LibraryStorageBar.tsx
@@ -0,0 +1,32 @@
+import { Progress } from '@librechat/client';
+import type { LibraryStorageInfo } from '~/types/library';
+import {
+ formatLibraryFileSize,
+ getStorageUsagePercent,
+} from './libraryUtils';
+
+interface LibraryStorageBarProps {
+ storage: LibraryStorageInfo;
+}
+
+export default function LibraryStorageBar({ storage }: LibraryStorageBarProps) {
+ const percent = getStorageUsagePercent(storage.usedBytes, storage.totalBytes);
+
+ return (
+
+
+ Storage
+
+ {formatLibraryFileSize(storage.usedBytes)} of {formatLibraryFileSize(storage.totalBytes)}{' '}
+ used
+
+
+
+
{percent}%
+
+ );
+}
\ No newline at end of file
diff --git a/client/src/components/Library/LibraryViewToggle.tsx b/client/src/components/Library/LibraryViewToggle.tsx
new file mode 100644
index 0000000..f46bf8c
--- /dev/null
+++ b/client/src/components/Library/LibraryViewToggle.tsx
@@ -0,0 +1,55 @@
+import { LayoutGrid, List } from 'lucide-react';
+import type { LibraryViewMode } from '~/types/library';
+import { cn } from '~/utils';
+
+interface LibraryViewToggleProps {
+ viewMode: LibraryViewMode;
+ onViewModeChange: (mode: LibraryViewMode) => void;
+ className?: string;
+}
+
+export default function LibraryViewToggle({
+ viewMode,
+ onViewModeChange,
+ className,
+}: LibraryViewToggleProps) {
+ return (
+
+
+
+
+ );
+}
diff --git a/client/src/components/Library/__tests__/LibraryList.test.tsx b/client/src/components/Library/__tests__/LibraryList.test.tsx
new file mode 100644
index 0000000..7cc6351
--- /dev/null
+++ b/client/src/components/Library/__tests__/LibraryList.test.tsx
@@ -0,0 +1,152 @@
+import { render, screen, within } from '@testing-library/react';
+import userEvent from '@testing-library/user-event';
+import '@testing-library/jest-dom';
+import { mockLibraryItems, mockLibraryStorage } from '~/data/mockLibraryData';
+import LibraryList from '../LibraryList';
+
+jest.mock('~/hooks', () => ({
+ useDebounce: (value: string) => value,
+}));
+
+describe('LibraryList', () => {
+ it('renders loading state', () => {
+ render();
+ expect(screen.getByRole('status', { name: 'Loading library' })).toBeInTheDocument();
+ });
+
+ it('renders error state with retry', () => {
+ const onRetry = jest.fn();
+ render(
+ ,
+ );
+
+ expect(screen.getByRole('alert')).toBeInTheDocument();
+ expect(screen.getByText('Network error')).toBeInTheDocument();
+ expect(screen.getByRole('button', { name: 'Retry loading library' })).toBeInTheDocument();
+ });
+
+ it('renders empty state', () => {
+ render();
+ expect(screen.getByText('Your library is empty')).toBeInTheDocument();
+ expect(screen.getByText(/64\.2 MB of 1\.0 GB used/)).toBeInTheDocument();
+ });
+
+ it('renders populated list with items', () => {
+ render(
+ ,
+ );
+
+ expect(screen.getByRole('search')).toBeInTheDocument();
+ expect(screen.getByText(`Showing ${mockLibraryItems.length} of ${mockLibraryItems.length} items`)).toBeInTheDocument();
+ expect(screen.getByText('Q4 Financial Report.pdf')).toBeInTheDocument();
+ });
+
+ it('filters items by search query', async () => {
+ render(
+ ,
+ );
+
+ const searchInput = screen.getByRole('searchbox', { name: 'Search library' });
+ await userEvent.type(searchInput, 'architecture');
+
+ expect(screen.getByText('architecture-diagram.pdf')).toBeInTheDocument();
+ expect(screen.queryByText('Q4 Financial Report.pdf')).not.toBeInTheDocument();
+ expect(screen.getByText(/Showing 1 of \d+ items/)).toBeInTheDocument();
+ });
+
+ it('filters items by type', async () => {
+ render(
+ ,
+ );
+
+ const typeDropdown = screen.getByRole('button', { name: /Filter by type/i });
+ await userEvent.click(typeDropdown);
+
+ const pdfOption = await screen.findByRole('option', { name: 'PDF' });
+ await userEvent.click(pdfOption);
+
+ expect(screen.getByText('Q4 Financial Report.pdf')).toBeInTheDocument();
+ expect(screen.getByText('architecture-diagram.pdf')).toBeInTheDocument();
+ expect(screen.queryByText('product-mockup-v3.png')).not.toBeInTheDocument();
+ });
+
+ it('sorts items by name', async () => {
+ render(
+ ,
+ );
+
+ const sortDropdown = screen.getByRole('button', { name: /Sort library items/i });
+ await userEvent.click(sortDropdown);
+
+ const nameOption = await screen.findByRole('option', { name: 'Name (A–Z)' });
+ await userEvent.click(nameOption);
+
+ const list = screen.getByRole('list', { name: 'Library items' });
+ const firstItem = within(list).getAllByRole('listitem')[0];
+ expect(firstItem).toHaveTextContent('README.md');
+ });
+
+ it('toggles between dense and preview views', async () => {
+ render(
+ ,
+ );
+
+ const list = screen.getByRole('list', { name: 'Library items' });
+ expect(list).toHaveClass('flex-col');
+
+ const previewButton = screen.getByRole('button', { name: 'Preview grid view' });
+ await userEvent.click(previewButton);
+
+ const grid = screen.getByRole('list', { name: 'Library items' });
+ expect(grid).toHaveClass('grid');
+ expect(screen.getByText('Q4 Financial Report.pdf')).toBeInTheDocument();
+
+ const denseButton = screen.getByRole('button', { name: 'Dense list view' });
+ await userEvent.click(denseButton);
+
+ expect(screen.getByRole('list', { name: 'Library items' })).toHaveClass('flex-col');
+ });
+
+ it('renders quick actions in dense view', () => {
+ render(
+ ,
+ );
+
+ const list = screen.getByRole('list', { name: 'Library items' });
+ const firstItem = within(list).getAllByRole('listitem')[0];
+
+ expect(within(firstItem).getByRole('button', { name: 'Open' })).toBeInTheDocument();
+ expect(within(firstItem).getByRole('button', { name: 'Download' })).toBeInTheDocument();
+ expect(within(firstItem).getByRole('button', { name: 'Share' })).toBeInTheDocument();
+ expect(within(firstItem).getByRole('button', { name: 'More options' })).toBeInTheDocument();
+ });
+});
diff --git a/client/src/components/Library/__tests__/libraryUtils.test.ts b/client/src/components/Library/__tests__/libraryUtils.test.ts
new file mode 100644
index 0000000..f37ed90
--- /dev/null
+++ b/client/src/components/Library/__tests__/libraryUtils.test.ts
@@ -0,0 +1,126 @@
+import { mockLibraryItems } from '~/data/mockLibraryData';
+import {
+ applyLibraryFilters,
+ filterLibraryItemsBySearch,
+ filterLibraryItemsByType,
+ findLibraryOption,
+ formatLibraryFileSize,
+ getStorageUsagePercent,
+ LIBRARY_SORT_OPTIONS,
+ LIBRARY_TYPE_FILTER_OPTIONS,
+ parseLibrarySortOption,
+ parseLibraryTypeFilter,
+ sortLibraryItems,
+} from '../libraryUtils';
+
+describe('libraryUtils', () => {
+ describe('formatLibraryFileSize', () => {
+ it('formats bytes', () => {
+ expect(formatLibraryFileSize(512)).toBe('512 B');
+ });
+
+ it('formats kilobytes', () => {
+ expect(formatLibraryFileSize(2048)).toBe('2.0 KB');
+ });
+
+ it('formats megabytes', () => {
+ expect(formatLibraryFileSize(2_621_440)).toBe('2.5 MB');
+ });
+ });
+
+ describe('getStorageUsagePercent', () => {
+ it('calculates percentage', () => {
+ expect(getStorageUsagePercent(50, 100)).toBe(50);
+ });
+
+ it('clamps to 100', () => {
+ expect(getStorageUsagePercent(150, 100)).toBe(100);
+ });
+ });
+
+ describe('filterLibraryItemsBySearch', () => {
+ it('returns all items for empty search', () => {
+ expect(filterLibraryItemsBySearch(mockLibraryItems, '')).toHaveLength(mockLibraryItems.length);
+ });
+
+ it('filters by name case-insensitively', () => {
+ const results = filterLibraryItemsBySearch(mockLibraryItems, 'pdf');
+ expect(results.every((item) => item.name.toLowerCase().includes('pdf') || item.description?.toLowerCase().includes('pdf'))).toBe(true);
+ });
+ });
+
+ describe('filterLibraryItemsByType', () => {
+ it('returns all items for "all" filter', () => {
+ expect(filterLibraryItemsByType(mockLibraryItems, 'all')).toHaveLength(mockLibraryItems.length);
+ });
+
+ it('filters by type', () => {
+ const results = filterLibraryItemsByType(mockLibraryItems, 'image');
+ expect(results.every((item) => item.type === 'image')).toBe(true);
+ });
+ });
+
+ describe('sortLibraryItems', () => {
+ it('sorts by newest first', () => {
+ const sorted = sortLibraryItems(mockLibraryItems, 'newest');
+ expect(new Date(sorted[0].createdAt).getTime()).toBeGreaterThanOrEqual(
+ new Date(sorted[1].createdAt).getTime(),
+ );
+ });
+
+ it('sorts by name', () => {
+ const sorted = sortLibraryItems(mockLibraryItems, 'name');
+ expect(sorted[0].name.localeCompare(sorted[1].name)).toBeLessThanOrEqual(0);
+ });
+
+ it('sorts by size descending', () => {
+ const sorted = sortLibraryItems(mockLibraryItems, 'size');
+ expect(sorted[0].sizeBytes).toBeGreaterThanOrEqual(sorted[1].sizeBytes);
+ });
+ });
+
+ describe('applyLibraryFilters', () => {
+ it('combines search, type filter, and sort', () => {
+ const results = applyLibraryFilters(mockLibraryItems, {
+ search: 'api',
+ typeFilter: 'code',
+ sortBy: 'name',
+ });
+
+ expect(results).toHaveLength(1);
+ expect(results[0].name).toBe('api-client.ts');
+ });
+ });
+
+ describe('parseLibraryTypeFilter', () => {
+ it('returns valid type filters unchanged', () => {
+ expect(parseLibraryTypeFilter('pdf')).toBe('pdf');
+ expect(parseLibraryTypeFilter('all')).toBe('all');
+ });
+
+ it('falls back to all for invalid values', () => {
+ expect(parseLibraryTypeFilter('invalid')).toBe('all');
+ });
+ });
+
+ describe('parseLibrarySortOption', () => {
+ it('returns valid sort options unchanged', () => {
+ expect(parseLibrarySortOption('name')).toBe('name');
+ expect(parseLibrarySortOption('newest')).toBe('newest');
+ });
+
+ it('falls back to newest for invalid values', () => {
+ expect(parseLibrarySortOption('invalid')).toBe('newest');
+ });
+ });
+
+ describe('findLibraryOption', () => {
+ it('finds matching option by value', () => {
+ expect(findLibraryOption(LIBRARY_TYPE_FILTER_OPTIONS, 'pdf').label).toBe('PDF');
+ });
+
+ it('returns fallback option when value is missing', () => {
+ expect(findLibraryOption(LIBRARY_SORT_OPTIONS, 'missing').value).toBe('newest');
+ });
+ });
+});
\ No newline at end of file
diff --git a/client/src/components/Library/index.ts b/client/src/components/Library/index.ts
new file mode 100644
index 0000000..58db893
--- /dev/null
+++ b/client/src/components/Library/index.ts
@@ -0,0 +1,10 @@
+export { default as LibraryList } from './LibraryList';
+export { default as LibraryListItem } from './LibraryListItem';
+export { default as LibraryFilters } from './LibraryFilters';
+export { default as LibraryEmptyState } from './LibraryEmptyState';
+export { default as LibraryLoadingState } from './LibraryLoadingState';
+export { default as LibraryErrorState } from './LibraryErrorState';
+export { default as LibraryStorageBar } from './LibraryStorageBar';
+export { default as LibraryListWithMockData } from './LibraryListWithMockData';
+export { useMockLibraryData } from './useMockLibraryData';
+export * from './libraryUtils';
\ No newline at end of file
diff --git a/client/src/components/Library/libraryTypeMeta.ts b/client/src/components/Library/libraryTypeMeta.ts
new file mode 100644
index 0000000..d491984
--- /dev/null
+++ b/client/src/components/Library/libraryTypeMeta.ts
@@ -0,0 +1,42 @@
+import {
+ AudioPaths,
+ CodePaths,
+ FilePaths,
+ SheetPaths,
+ TextPaths,
+ VideoPaths,
+} from '@librechat/client';
+import type { LibraryItemType } from '~/types/library';
+
+export interface LibraryFileType {
+ fill: string;
+ paths: React.FC;
+ title: string;
+}
+
+export const TYPE_FILE_TYPE_MAP: Record, LibraryFileType> = {
+ pdf: { paths: TextPaths, fill: '#EF4444', title: 'PDF' },
+ image: { paths: FilePaths, fill: '#EC4899', title: 'Image' },
+ video: { paths: VideoPaths, fill: '#A855F7', title: 'Video' },
+ document: { paths: TextPaths, fill: '#3B82F6', title: 'Document' },
+ code: { paths: CodePaths, fill: '#F97316', title: 'Code' },
+ spreadsheet: { paths: SheetPaths, fill: '#10B981', title: 'Spreadsheet' },
+ audio: { paths: AudioPaths, fill: '#F59E0B', title: 'Audio' },
+};
+
+export const TYPE_COLOR_MAP: Record = {
+ pdf: 'text-red-500',
+ image: 'text-pink-500',
+ video: 'text-purple-500',
+ document: 'text-blue-500',
+ code: 'text-orange-500',
+ spreadsheet: 'text-emerald-500',
+ audio: 'text-amber-500',
+ artifact: 'text-violet-500',
+};
+
+export function getLibraryFileType(
+ type: LibraryItemType,
+): LibraryFileType | null {
+ return type === 'artifact' ? null : TYPE_FILE_TYPE_MAP[type];
+}
diff --git a/client/src/components/Library/libraryUtils.ts b/client/src/components/Library/libraryUtils.ts
new file mode 100644
index 0000000..0b284c5
--- /dev/null
+++ b/client/src/components/Library/libraryUtils.ts
@@ -0,0 +1,164 @@
+import type { Option } from '~/common';
+import type {
+ LibraryFiltersState,
+ LibraryItem,
+ LibraryItemType,
+ LibrarySortOption,
+ LibraryTypeFilter,
+} from '~/types/library';
+
+/** Human-readable labels for each library item type. */
+export const LIBRARY_TYPE_LABELS: Record = {
+ pdf: 'PDF',
+ image: 'Image',
+ video: 'Video',
+ document: 'Document',
+ code: 'Code',
+ spreadsheet: 'Spreadsheet',
+ audio: 'Audio',
+ artifact: 'Artifact',
+};
+
+/** Sort option labels for the sort dropdown. */
+export const LIBRARY_SORT_LABELS: Record = {
+ newest: 'Newest first',
+ oldest: 'Oldest first',
+ name: 'Name (A–Z)',
+ size: 'Size (largest)',
+};
+
+/** All filterable types in display order. */
+export const LIBRARY_FILTER_TYPES: LibraryItemType[] = [
+ 'pdf',
+ 'image',
+ 'video',
+ 'document',
+ 'code',
+ 'spreadsheet',
+ 'audio',
+ 'artifact',
+];
+
+const LIBRARY_TYPE_FILTER_VALUES = new Set(['all', ...LIBRARY_FILTER_TYPES]);
+const LIBRARY_SORT_OPTION_VALUES = new Set(Object.keys(LIBRARY_SORT_LABELS));
+
+/** Dropdown options for the type filter control. */
+export const LIBRARY_TYPE_FILTER_OPTIONS: Option[] = [
+ { value: 'all', label: 'All types' },
+ ...LIBRARY_FILTER_TYPES.map((type) => ({
+ value: type,
+ label: LIBRARY_TYPE_LABELS[type],
+ })),
+];
+
+/** Dropdown options for the sort control. */
+export const LIBRARY_SORT_OPTIONS: Option[] = (
+ Object.entries(LIBRARY_SORT_LABELS) as [LibrarySortOption, string][]
+).map(([value, label]) => ({ value, label }));
+
+/** Resolves a dropdown value to a valid type filter, falling back to `all`. */
+export function parseLibraryTypeFilter(value: string): LibraryTypeFilter {
+ return LIBRARY_TYPE_FILTER_VALUES.has(value) ? (value as LibraryTypeFilter) : 'all';
+}
+
+/** Resolves a dropdown value to a valid sort option, falling back to `newest`. */
+export function parseLibrarySortOption(value: string): LibrarySortOption {
+ return LIBRARY_SORT_OPTION_VALUES.has(value) ? (value as LibrarySortOption) : 'newest';
+}
+
+/** Finds the matching dropdown option or returns the fallback entry. */
+export function findLibraryOption(
+ options: Option[],
+ value: string,
+ fallbackIndex = 0,
+): Option {
+ return options.find((option) => String(option.value) === value) ?? options[fallbackIndex];
+}
+
+/** Formats byte count into a readable string with unit suffix. */
+export function formatLibraryFileSize(bytes: number): string {
+ if (bytes >= 1_073_741_824) {
+ return `${(bytes / 1_073_741_824).toFixed(1)} GB`;
+ }
+ if (bytes >= 1_048_576) {
+ return `${(bytes / 1_048_576).toFixed(1)} MB`;
+ }
+ if (bytes >= 1024) {
+ return `${(bytes / 1024).toFixed(1)} KB`;
+ }
+ return `${bytes} B`;
+}
+
+/** Formats an ISO date string for list display. */
+export function formatLibraryDate(isoDate: string): string {
+ return new Intl.DateTimeFormat(undefined, {
+ year: 'numeric',
+ month: 'short',
+ day: 'numeric',
+ }).format(new Date(isoDate));
+}
+
+/** Returns the storage usage percentage, clamped to 0–100. */
+export function getStorageUsagePercent(usedBytes: number, totalBytes: number): number {
+ if (totalBytes <= 0) {
+ return 0;
+ }
+ return Math.max(0, Math.min(100, Math.round((usedBytes / totalBytes) * 100)));
+}
+
+/** Filters items by case-insensitive search across name and description. */
+export function filterLibraryItemsBySearch(items: LibraryItem[], search: string): LibraryItem[] {
+ const query = search.trim().toLowerCase();
+ if (!query) {
+ return items;
+ }
+
+ return items.filter(
+ (item) =>
+ item.name.toLowerCase().includes(query) ||
+ item.description?.toLowerCase().includes(query),
+ );
+}
+
+/** Filters items by content type. */
+export function filterLibraryItemsByType(
+ items: LibraryItem[],
+ typeFilter: LibraryTypeFilter,
+): LibraryItem[] {
+ if (typeFilter === 'all') {
+ return items;
+ }
+ return items.filter((item) => item.type === typeFilter);
+}
+
+/** Sorts items by the selected sort option. */
+export function sortLibraryItems(items: LibraryItem[], sortBy: LibrarySortOption): LibraryItem[] {
+ const sorted = [...items];
+
+ switch (sortBy) {
+ case 'newest':
+ return sorted.sort(
+ (a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime(),
+ );
+ case 'oldest':
+ return sorted.sort(
+ (a, b) => new Date(a.createdAt).getTime() - new Date(b.createdAt).getTime(),
+ );
+ case 'name':
+ return sorted.sort((a, b) => a.name.localeCompare(b.name, undefined, { sensitivity: 'base' }));
+ case 'size':
+ return sorted.sort((a, b) => b.sizeBytes - a.sizeBytes);
+ default:
+ return sorted;
+ }
+}
+
+/** Applies search, type filter, and sort in sequence. */
+export function applyLibraryFilters(
+ items: LibraryItem[],
+ filters: LibraryFiltersState,
+): LibraryItem[] {
+ const searched = filterLibraryItemsBySearch(items, filters.search);
+ const filtered = filterLibraryItemsByType(searched, filters.typeFilter);
+ return sortLibraryItems(filtered, filters.sortBy);
+}
\ No newline at end of file
diff --git a/client/src/components/Library/useMockLibraryData.ts b/client/src/components/Library/useMockLibraryData.ts
new file mode 100644
index 0000000..f89498e
--- /dev/null
+++ b/client/src/components/Library/useMockLibraryData.ts
@@ -0,0 +1,103 @@
+import { useCallback, useEffect, useState } from 'react';
+import { mockLibraryResponse } from '~/data/mockLibraryData';
+import type {
+ LibraryItem,
+ LibraryListStatus,
+ LibraryStorageInfo,
+} from '~/types/library';
+
+const MOCK_LOAD_DELAY_MS = 800;
+
+export type MockLibraryScenario = 'success' | 'empty' | 'error';
+
+interface UseMockLibraryDataOptions {
+ /** Simulated network scenario. Defaults to `success`. */
+ scenario?: MockLibraryScenario;
+ /** Artificial delay before resolving mock data (ms). */
+ delayMs?: number;
+ /** When true, fetches on mount. Defaults to true. */
+ autoFetch?: boolean;
+}
+
+interface UseMockLibraryDataResult {
+ items: LibraryItem[];
+ storage: LibraryStorageInfo | null;
+ status: LibraryListStatus;
+ errorMessage: string | null;
+ isLoading: boolean;
+ refetch: () => void;
+}
+
+/**
+ * Simulates an async library API call with mocked data.
+ * Useful for development and Storybook-style previews before the real API exists.
+ */
+export function useMockLibraryData(
+ options: UseMockLibraryDataOptions = {},
+): UseMockLibraryDataResult {
+ const { scenario = 'success', delayMs = MOCK_LOAD_DELAY_MS, autoFetch = true } = options;
+
+ const [items, setItems] = useState([]);
+ const [storage, setStorage] = useState(null);
+ const [status, setStatus] = useState('loading');
+ const [errorMessage, setErrorMessage] = useState(null);
+ const [fetchKey, setFetchKey] = useState(0);
+
+ const refetch = useCallback(() => {
+ setFetchKey((key) => key + 1);
+ }, []);
+
+ useEffect(() => {
+ if (!autoFetch) {
+ return;
+ }
+
+ let cancelled = false;
+
+ const load = async () => {
+ setStatus('loading');
+ setErrorMessage(null);
+
+ await new Promise((resolve) => setTimeout(resolve, delayMs));
+
+ if (cancelled) {
+ return;
+ }
+
+ if (scenario === 'error') {
+ setItems([]);
+ setStorage(null);
+ setStatus('error');
+ setErrorMessage('Failed to connect to the library service. Please try again.');
+ return;
+ }
+
+ if (scenario === 'empty') {
+ setItems([]);
+ setStorage(mockLibraryResponse.storage);
+ setStatus('empty');
+ return;
+ }
+
+ const response = mockLibraryResponse;
+ setItems(response.items);
+ setStorage(response.storage);
+ setStatus(response.items.length > 0 ? 'populated' : 'empty');
+ };
+
+ load();
+
+ return () => {
+ cancelled = true;
+ };
+ }, [autoFetch, delayMs, scenario, fetchKey]);
+
+ return {
+ items,
+ storage,
+ status,
+ errorMessage,
+ isLoading: status === 'loading',
+ refetch,
+ };
+}
\ No newline at end of file
diff --git a/client/src/data/mockLibraryData.ts b/client/src/data/mockLibraryData.ts
new file mode 100644
index 0000000..e8fe8a5
--- /dev/null
+++ b/client/src/data/mockLibraryData.ts
@@ -0,0 +1,147 @@
+import type { LibraryItem, LibraryListResponse, LibraryStorageInfo } from '~/types/library';
+
+/** Mock library items covering all supported types and both kinds. */
+export const mockLibraryItems: LibraryItem[] = [
+ {
+ id: 'lib-001',
+ name: 'Q4 Financial Report.pdf',
+ type: 'pdf',
+ kind: 'file',
+ sizeBytes: 2_458_880,
+ createdAt: '2026-05-28T14:22:00.000Z',
+ description: 'Quarterly financial summary and projections',
+ mimeType: 'application/pdf',
+ },
+ {
+ id: 'lib-002',
+ name: 'product-mockup-v3.png',
+ type: 'image',
+ kind: 'file',
+ sizeBytes: 1_572_864,
+ createdAt: '2026-06-01T09:15:00.000Z',
+ description: 'Latest product UI mockup',
+ mimeType: 'image/png',
+ },
+ {
+ id: 'lib-003',
+ name: 'onboarding-demo.mp4',
+ type: 'video',
+ kind: 'file',
+ sizeBytes: 45_875_200,
+ createdAt: '2026-05-15T11:30:00.000Z',
+ description: 'Product onboarding walkthrough',
+ mimeType: 'video/mp4',
+ },
+ {
+ id: 'lib-004',
+ name: 'Project Proposal.docx',
+ type: 'document',
+ kind: 'file',
+ sizeBytes: 524_288,
+ createdAt: '2026-04-20T16:45:00.000Z',
+ description: 'Client project proposal draft',
+ mimeType: 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
+ },
+ {
+ id: 'lib-005',
+ name: 'api-client.ts',
+ type: 'code',
+ kind: 'artifact',
+ sizeBytes: 12_400,
+ createdAt: '2026-06-08T08:10:00.000Z',
+ description: 'Generated TypeScript API client',
+ conversationId: 'conv-abc123',
+ mimeType: 'text/typescript',
+ },
+ {
+ id: 'lib-006',
+ name: 'sales-data-2026.xlsx',
+ type: 'spreadsheet',
+ kind: 'file',
+ sizeBytes: 892_160,
+ createdAt: '2026-05-02T13:00:00.000Z',
+ description: 'Annual sales data export',
+ mimeType: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
+ },
+ {
+ id: 'lib-007',
+ name: 'meeting-recording.mp3',
+ type: 'audio',
+ kind: 'file',
+ sizeBytes: 8_388_608,
+ createdAt: '2026-03-18T10:20:00.000Z',
+ description: 'Team standup recording',
+ mimeType: 'audio/mpeg',
+ },
+ {
+ id: 'lib-008',
+ name: 'dashboard-component.tsx',
+ type: 'artifact',
+ kind: 'artifact',
+ sizeBytes: 18_750,
+ createdAt: '2026-06-09T17:45:00.000Z',
+ description: 'React dashboard component generated in chat',
+ conversationId: 'conv-def456',
+ mimeType: 'text/typescript',
+ },
+ {
+ id: 'lib-009',
+ name: 'architecture-diagram.pdf',
+ type: 'pdf',
+ kind: 'file',
+ sizeBytes: 1_048_576,
+ createdAt: '2026-02-10T12:00:00.000Z',
+ description: 'System architecture overview',
+ mimeType: 'application/pdf',
+ },
+ {
+ id: 'lib-010',
+ name: 'utils.py',
+ type: 'code',
+ kind: 'artifact',
+ sizeBytes: 6_200,
+ createdAt: '2026-06-07T14:30:00.000Z',
+ description: 'Python utility functions',
+ conversationId: 'conv-ghi789',
+ mimeType: 'text/x-python',
+ },
+ {
+ id: 'lib-011',
+ name: 'team-photo.jpg',
+ type: 'image',
+ kind: 'file',
+ sizeBytes: 3_145_728,
+ createdAt: '2026-01-25T18:00:00.000Z',
+ description: 'Company team photo',
+ mimeType: 'image/jpeg',
+ },
+ {
+ id: 'lib-012',
+ name: 'README.md',
+ type: 'document',
+ kind: 'artifact',
+ sizeBytes: 4_800,
+ createdAt: '2026-06-06T11:00:00.000Z',
+ description: 'Project documentation generated in chat',
+ conversationId: 'conv-jkl012',
+ mimeType: 'text/markdown',
+ },
+];
+
+/** Mock storage quota — 64.2 MB used of 1 GB. */
+export const mockLibraryStorage: LibraryStorageInfo = {
+ usedBytes: 67_289_344,
+ totalBytes: 1_073_741_824,
+};
+
+/** Full mock API response shape. */
+export const mockLibraryResponse: LibraryListResponse = {
+ items: mockLibraryItems,
+ storage: mockLibraryStorage,
+};
+
+/** Empty library response for testing empty state. */
+export const mockEmptyLibraryResponse: LibraryListResponse = {
+ items: [],
+ storage: { usedBytes: 0, totalBytes: 1_073_741_824 },
+};
\ No newline at end of file
diff --git a/client/src/types/library.ts b/client/src/types/library.ts
new file mode 100644
index 0000000..743a53b
--- /dev/null
+++ b/client/src/types/library.ts
@@ -0,0 +1,58 @@
+/** Supported library item content types for filtering. */
+export type LibraryItemType =
+ | 'pdf'
+ | 'image'
+ | 'video'
+ | 'document'
+ | 'code'
+ | 'spreadsheet'
+ | 'audio'
+ | 'artifact';
+
+/** Distinguishes uploaded files from generated artifacts. */
+export type LibraryItemKind = 'file' | 'artifact';
+
+/** Available sort options for the library list. */
+export type LibrarySortOption = 'newest' | 'oldest' | 'name' | 'size';
+
+/** View modes for the library artifact list. */
+export type LibraryViewMode = 'dense' | 'preview';
+
+/** UI state for the library list container. */
+export type LibraryListStatus = 'loading' | 'empty' | 'populated' | 'error';
+
+/** Type filter value — `all` shows every item. */
+export type LibraryTypeFilter = LibraryItemType | 'all';
+
+export interface LibraryItem {
+ id: string;
+ name: string;
+ type: LibraryItemType;
+ kind: LibraryItemKind;
+ sizeBytes: number;
+ createdAt: string;
+ updatedAt?: string;
+ description?: string;
+ conversationId?: string;
+ mimeType?: string;
+}
+
+export interface LibraryStorageInfo {
+ usedBytes: number;
+ totalBytes: number;
+}
+
+export interface LibraryFiltersState {
+ search: string;
+ typeFilter: LibraryTypeFilter;
+ sortBy: LibrarySortOption;
+}
+
+export interface LibraryViewState {
+ viewMode: LibraryViewMode;
+}
+
+export interface LibraryListResponse {
+ items: LibraryItem[];
+ storage: LibraryStorageInfo | null;
+}
\ No newline at end of file