diff --git a/frontend/src/app/pages/Directory/DirectoryFilterBar.tsx b/frontend/src/app/pages/Directory/DirectoryFilterBar.tsx index 0c067a31..698f250f 100644 --- a/frontend/src/app/pages/Directory/DirectoryFilterBar.tsx +++ b/frontend/src/app/pages/Directory/DirectoryFilterBar.tsx @@ -103,12 +103,14 @@ interface Props { sortOptions: PickerOption[]; sortValue: string; onSort: (v: string) => void; + // Optional left-hand content for the controls row; without it that band is empty with two pills pushed to the far right. + leading?: React.ReactNode; } // The Directory's search row + chip/filter row, shared by both tabs (same chrome on claude.ai). const DirectoryFilterBar: React.FC = ({ searchPlaceholder, query, onQuery, - filterSections, filterSelected, onToggleFilter, sortOptions, sortValue, onSort, + filterSections, filterSelected, onToggleFilter, sortOptions, sortValue, onSort, leading, }) => { const c = useClaudeTokens(); return ( @@ -135,7 +137,8 @@ const DirectoryFilterBar: React.FC = ({ }, }} /> - + + {leading} diff --git a/frontend/src/app/pages/Directory/DirectoryPackagesTab.tsx b/frontend/src/app/pages/Directory/DirectoryPackagesTab.tsx index 32e4b219..2bd9b557 100644 --- a/frontend/src/app/pages/Directory/DirectoryPackagesTab.tsx +++ b/frontend/src/app/pages/Directory/DirectoryPackagesTab.tsx @@ -2,6 +2,7 @@ import React, { useEffect, useMemo, useState } from 'react'; import Box from '@mui/material/Box'; import Alert from '@mui/material/Alert'; import Snackbar from '@mui/material/Snackbar'; +import Stack from '@mui/material/Stack'; import Typography from '@mui/material/Typography'; import CircularProgress from '@mui/material/CircularProgress'; import { useClaudeTokens } from '@/shared/styles/ThemeContext'; @@ -122,6 +123,20 @@ const DirectoryPackagesTab: React.FC<{ onInstalled?: (rootType: string) => void } }; + const sectionLabel = (label: string, count?: number): React.ReactElement | null => { + if (!label) return null; + return ( + + + {label} + + {typeof count === 'number' && ( + {count} + )} + + ); + }; + const body = (): React.ReactElement => { if (loading && !loaded) { return ( @@ -140,11 +155,9 @@ const DirectoryPackagesTab: React.FC<{ onInstalled?: (rootType: string) => void return ( <> {visibleBundles.length > 0 && ( - - - Collections - - + + {sectionLabel('Collections')} + {visibleBundles.map((bundle) => ( void )} - + {visible.length > 0 && sectionLabel(visibleBundles.length > 0 ? 'All packages' : '', visible.length)} + {visible.map((listing) => ( void ]} sortValue={sort} onSort={setSort} + leading={ + loaded ? ( + + {visible.length + visibleBundles.length} {visible.length + visibleBundles.length === 1 ? 'result' : 'results'} + + ) : null + } /> {source === 'cache' && ( diff --git a/frontend/src/app/pages/Directory/packages/PackageBundleCard.tsx b/frontend/src/app/pages/Directory/packages/PackageBundleCard.tsx index bfdc7787..9b7c2b21 100644 --- a/frontend/src/app/pages/Directory/packages/PackageBundleCard.tsx +++ b/frontend/src/app/pages/Directory/packages/PackageBundleCard.tsx @@ -2,11 +2,11 @@ import React from 'react'; import Box from '@mui/material/Box'; import Stack from '@mui/material/Stack'; import Typography from '@mui/material/Typography'; -import Chip from '@mui/material/Chip'; -import Inventory2Icon from '@mui/icons-material/Inventory2'; +import ChevronRightIcon from '@mui/icons-material/ChevronRight'; import ExtensionIcon from '@mui/icons-material/Extension'; +import Inventory2Icon from '@mui/icons-material/Inventory2Outlined'; import { useClaudeTokens } from '@/shared/styles/ThemeContext'; -import { KIND_LABELS, type Listing } from './catalog'; +import { type Listing } from './catalog'; interface Props { bundle: Listing; @@ -14,60 +14,58 @@ interface Props { onOpen: () => void; } -// Leads with a stacked-icon motif of its first few members and a count, so a bundle reads as a collection at a glance rather than as another single package. +// A collection is one wide row, not a card in the package grid: it groups the things below it, and a +// half-width card with empty space beside it reads as a layout that broke rather than a section. export default function PackageBundleCard({ bundle, members, onOpen }: Props) { const c = useClaudeTokens(); const preview = members.slice(0, 4); - const kinds = Array.from(new Set(members.map((m) => KIND_LABELS[m.kind] || m.kind).filter(Boolean))); + const names = members.map((m) => m.title).filter(Boolean); return ( - - - {bundle.icon_url ? ( - - ) : ( - - )} - - + + {bundle.icon_url ? ( + + ) : ( + + )} + + + + {bundle.title} - - Bundle · {members.length} {members.length === 1 ? 'package' : 'packages'} + + {members.length} {members.length === 1 ? 'package' : 'packages'} - - + + + {bundle.description || names.join(', ') || 'A curated collection of packages.'} + + - - {bundle.description || 'A curated collection of packages.'} - - - + {preview.map((m, i) => ( ) : ( - + )} ))} {members.length > preview.length && ( - - +{members.length - preview.length} more + + +{members.length - preview.length} )} - {kinds.length > 0 && ( - - )} + + ); } diff --git a/frontend/src/app/pages/Directory/packages/PackageCard.tsx b/frontend/src/app/pages/Directory/packages/PackageCard.tsx index 5812801f..d4b878ed 100644 --- a/frontend/src/app/pages/Directory/packages/PackageCard.tsx +++ b/frontend/src/app/pages/Directory/packages/PackageCard.tsx @@ -2,7 +2,6 @@ import React from 'react'; import Box from '@mui/material/Box'; import Stack from '@mui/material/Stack'; import Typography from '@mui/material/Typography'; -import Chip from '@mui/material/Chip'; import ExtensionIcon from '@mui/icons-material/Extension'; import { useClaudeTokens } from '@/shared/styles/ThemeContext'; import { parseTags, KIND_LABELS, type Listing } from './catalog'; @@ -25,44 +24,41 @@ export default function PackageCard({ listing, onOpen, onTag }: Props) { flexDirection: 'column', cursor: 'pointer', bgcolor: c.bg.surface, - border: `1px solid ${c.border.subtle}`, - borderRadius: 3, - p: 2.5, + border: `${c.border.width} solid ${c.border.subtle}`, + borderRadius: `${c.radius.lg}px`, + p: 2, transition: c.transition, - '&:hover': { - borderColor: c.border.medium, - boxShadow: c.shadow.md, - transform: 'translateY(-2px)', - }, + // No lift on hover: every other surface in this app answers with the border and a hairline shadow, and a card that jumps reads cheap next to them. + '&:hover': { borderColor: c.border.strong, boxShadow: c.shadow.sm }, }} > - + {listing.icon_url ? ( ) : ( - + )} {listing.title} - + {KIND_LABELS[listing.kind] || listing.kind || 'Package'} {listing.version ? ` · v${listing.version}` : ''} + {listing.author ? ` · ${listing.author}` : ''} {listing.description || 'No description provided.'} - + {tags.map((t) => ( - ) => { + role="button" + onClick={(e: React.MouseEvent) => { e.stopPropagation(); onTag(t); }} sx={{ - height: 22, - fontSize: '0.72rem', - bgcolor: c.bg.secondary, - color: c.text.tertiary, - '&:hover': { color: c.text.primary }, + px: 0.9, + py: 0.2, + borderRadius: `${c.radius.sm}px`, + border: `${c.border.width} solid ${c.border.subtle}`, + fontSize: '0.7188rem', + color: c.text.muted, + transition: c.transition, + '&:hover': { color: c.text.secondary, borderColor: c.border.medium }, }} - /> + > + {t} + ))}