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
3 changes: 2 additions & 1 deletion webui/src/components/category-card.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,8 @@ export const CategoryCard: FunctionComponent<CategoryCardProps> = ({ label, icon
sx={{
width: '1.875rem',
height: '1.875rem',
borderRadius: '8px',
// sx multiplies by theme.shape.borderRadius
borderRadius: 1,
bgcolor: 'accentSoft',
color: 'secondary.main',
display: 'flex',
Expand Down
2 changes: 1 addition & 1 deletion webui/src/components/category-pill.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ const Root = styled(ButtonBase, {
fontSize: '0.8125rem',
fontWeight: isSelected ? 600 : 500,
padding: '0.4375rem 0.8125rem',
borderRadius: '999px',
borderRadius: theme.shape.borderRadiusPill,
whiteSpace: 'nowrap',
fontFamily: 'inherit',
transition: 'border-color 0.14s, color 0.14s',
Expand Down
156 changes: 127 additions & 29 deletions webui/src/components/extension-card.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import { Link as RouteLink } from 'react-router-dom';
import { Paper, Typography, Box, Fade, Skeleton } from '@mui/material';
import { CSSObject, styled, Theme } from '@mui/material/styles';
import SaveAltIcon from '@mui/icons-material/SaveAlt';
import VerifiedIcon from '@mui/icons-material/Verified';
import { ExtensionDetailRoutes } from '../pages/extension-detail/extension-detail-routes';
import { SearchEntry } from '../extension-registry-types';
import { ExtensionIcon } from './extension/extension-icon';
Expand All @@ -26,15 +27,17 @@ import { GridItemProps } from '../hooks/use-grid-cursor';
import { cardHoverLift, cardSurface, focusRing } from './page-primitives';

// Shared surface + footprint so the card and its skeleton occupy identical space.
// A size container so tight cards can shed the footer's review count.
const cardLayout = (theme: Theme): CSSObject => ({
...cardSurface(theme),
padding: '1.375rem 1rem',
[theme.breakpoints.down('sm')]: { padding: '0.875rem 0.625rem' },
containerType: 'inline-size',
padding: '1rem 0.875rem',
[theme.breakpoints.down('sm')]: { padding: '0.75rem 0.625rem' },
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
height: '100%',
minHeight: '12.875rem'
minHeight: '13rem'
});

const CardRoot = styled(Paper)(({ theme }) => ({
Expand All @@ -54,14 +57,15 @@ const SkeletonRoot = styled(Paper)(({ theme }) => cardLayout(theme));
// Only the unknown parts are skeletons; the stars' empty state looks the same loaded or not.
const SkeletonContent: FunctionComponent = () => (
<>
<Skeleton variant='rounded' width={54} height={54} sx={{ flexShrink: 0, mb: '0.875rem' }} />
<Box sx={{ width: '100%', height: { xs: '2.125rem', sm: '2.375rem' }, overflow: 'hidden' }}>
<Skeleton variant='text' sx={{ fontSize: { xs: '0.8125rem', sm: '0.875rem' } }} />
<Skeleton variant='text' width='60%' sx={{ fontSize: { xs: '0.8125rem', sm: '0.875rem' }, mx: 'auto' }} />
<Skeleton variant='rounded' width={54} height={54} sx={{ flexShrink: 0, mb: '0.75rem' }} />
<Skeleton variant='text' width='70%' sx={{ fontSize: { xs: '0.8125rem', sm: '0.875rem' } }} />
<Box sx={{ width: '100%', height: '2.1rem', mt: '0.375rem', overflow: 'hidden' }}>
<Skeleton variant='text' sx={{ fontSize: '0.75rem' }} />
<Skeleton variant='text' width='60%' sx={{ fontSize: '0.75rem', mx: 'auto' }} />
</Box>
<Box sx={{ width: '100%', mt: '0.875rem', display: 'flex', justifyContent: 'space-between', gap: 1 }}>
<Box sx={{ width: '100%', mt: '0.75rem', display: 'flex', justifyContent: 'space-between', gap: 1 }}>
<Skeleton variant='text' width='45%' sx={{ fontSize: '0.75rem' }} />
<Skeleton variant='text' width='25%' sx={{ fontSize: '0.6875rem' }} />
<Skeleton variant='text' width='30%' sx={{ fontSize: '0.6875rem' }} />
</Box>
<Box
sx={{
Expand All @@ -72,7 +76,8 @@ const SkeletonContent: FunctionComponent = () => (
borderColor: 'border2',
display: 'flex',
alignItems: 'center',
fontSize: { xs: '0.875rem', sm: '1.25rem' }
gap: '0.0625rem',
fontSize: { xs: '0.8125rem', sm: '0.875rem' }
}}>
<ExtensionRatingStars number={0} fontSize='inherit' />
</Box>
Expand Down Expand Up @@ -107,6 +112,7 @@ export const ExtensionCard = memo(
) {
const title = extension?.displayName ?? extension?.name;
const downloadCount = extension ? formatCompactNumber(extension.downloadCount ?? 0) : undefined;
const reviewCount = extension?.reviewCount ?? 0;

// One Fade over both states so it runs once and carries through the skeleton → card swap.
return (
Expand All @@ -130,7 +136,7 @@ export const ExtensionCard = memo(
sx={{
width: 54,
height: 54,
mb: '0.875rem'
mb: '0.75rem'
}}>
<ExtensionIcon
extension={extension}
Expand All @@ -139,51 +145,122 @@ export const ExtensionCard = memo(
/>
</Box>
<Typography
noWrap
sx={{
fontSize: { xs: '0.8125rem', sm: '0.875rem' },
fontWeight: 700,
fontWeight: 600,
lineHeight: 1.3,
width: '100%'
}}>
{title}
</Typography>
{/* Fixed two-line box so the meta rows align across cards with short descriptions. */}
<Typography
sx={{
fontSize: '0.75rem',
color: 'text.secondary',
lineHeight: 1.4,
height: '2.1rem',
mt: '0.375rem',
width: '100%',
minHeight: { xs: '2.125rem', sm: '2.375rem' },
display: '-webkit-box',
WebkitLineClamp: 2,
WebkitBoxOrient: 'vertical',
overflow: 'hidden'
}}>
{title}
{extension.description}
</Typography>
<Box
sx={{
width: '100%',
mt: '0.875rem',
mt: '0.75rem',
display: 'flex',
alignItems: 'center',
justifyContent: 'space-between',
gap: 1
gap: '0.375rem',
overflow: 'hidden',
// Hovering a truncated side unfolds it; its neighbor yields instead.
'& > :first-of-type:hover': { flexShrink: 0, maxWidth: 'none' },
'& > :first-of-type:hover + *': { minWidth: 0, flexShrink: 1 },
'& > :last-of-type:hover': { flexShrink: 0, maxWidth: 'none' },
'&:has(> :last-of-type:hover) > :first-of-type': {
flexShrink: 1,
minWidth: 0
}
}}>
<Typography
component='div'
noWrap
<Box
sx={{
fontSize: '0.75rem',
color: 'text.disabled',
display: 'flex',
alignItems: 'center',
gap: '0.25rem',
minWidth: 0,
textAlign: 'left'
// Both sides trim under pressure, but the namespace keeps
// priority: it shrinks at a third of the version's rate.
flexShrink: 1,
maxWidth: '70%',
// When the version unfolds and this side collapses, clip
// the badge instead of letting it paint over the version.
overflow: 'hidden'
}}>
{extension.namespace}
</Typography>
<Typography
component='div'
noWrap
sx={{
fontSize: '0.75rem',
color: 'text.disabled',
minWidth: 0,
textAlign: 'left'
}}>
{extension.namespace}
</Typography>
{extension.verified && (
<Box
component='span'
title='Verified publisher'
role='img'
aria-label='Verified publisher'
sx={{
display: 'inline-flex',
fontSize: '0.75rem',
color: 'secondary.light',
flexShrink: 0
}}>
<VerifiedIcon fontSize='inherit' />
</Box>
)}
</Box>
<Typography
component='div'
noWrap
title={extension.version}
sx={{
fontSize: '0.6875rem',
color: 'text.disabled',
flexShrink: 0,
fontFamily: MONO_FONT
// Yields faster than the namespace, down to one glyph
// plus the ellipsis; long calendar versions cap at 10ch.
flexShrink: 3,
minWidth: '2ch',
maxWidth: '10ch',
fontFamily: MONO_FONT,
whiteSpace: 'nowrap',
overflow: 'hidden',
textOverflow: 'ellipsis'
}}>
{extension.version}
</Typography>
</Box>
{extension.deprecated && (
<Box
sx={{
width: '100%',
mt: '0.25rem',
textAlign: 'left',
fontSize: '0.6875rem',
fontWeight: 500,
color: 'warningAccent'
}}>
deprecated
</Box>
)}
<Box
sx={{
width: '100%',
Expand All @@ -196,19 +273,40 @@ export const ExtensionCard = memo(
justifyContent: 'space-between',
fontSize: '0.75rem'
}}>
<Box sx={{ display: 'flex', fontSize: { xs: '0.875rem', sm: '1.25rem' } }}>
<Box
sx={{
display: 'flex',
alignItems: 'center',
gap: '0.0625rem',
fontSize: { xs: '0.8125rem', sm: '0.875rem' }
}}>
<ExtensionRatingStars
number={extension.averageRating ?? 0}
fontSize='inherit'
/>
{reviewCount > 0 && (
<Box
component='span'
sx={{
fontSize: '0.6875rem',
color: 'text.disabled',
ml: '0.1875rem',
// Sheds first on tight cards; the query measures
// the card's content box, i.e. this row's width.
'@container (max-width: 134px)': { display: 'none' }
}}>
({formatCompactNumber(reviewCount)})
</Box>
)}
</Box>
{downloadCount !== '0' && (
<Box
component='span'
sx={{
display: 'flex',
alignItems: 'center',
gap: '0.25rem',
gap: '0.1875rem',
flexShrink: 0,
fontFamily: MONO_FONT,
fontSize: '0.6875rem',
color: 'text.disabled'
Expand Down
15 changes: 2 additions & 13 deletions webui/src/components/extension-list.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@

import { FunctionComponent, useContext, useEffect, useMemo, useRef } from 'react';
import InfiniteScroll from 'react-infinite-scroller';
import { Box } from '@mui/material';
import { ExtensionGrid } from './page-primitives';
import { ExtensionCard } from './extension-card';
import { ExtensionFilter } from '../extension-registry-service';
import { useExtensionResultsCursor } from '../hooks/use-extension-results-cursor';
Expand Down Expand Up @@ -79,18 +79,7 @@ export const ExtensionList: FunctionComponent<ExtensionListProps> = ({ filter, o
}}
hasMore={hasNextPage && !isFetchingNextPage}
threshold={200}>
<Box
{...grid.containerProps}
sx={{
display: 'grid',
gridTemplateColumns: {
xs: 'repeat(2, minmax(0, 1fr))',
sm: 'repeat(auto-fill, minmax(175px, 1fr))'
},
gap: '1rem'
}}>
{cards}
</Box>
<ExtensionGrid {...grid.containerProps}>{cards}</ExtensionGrid>
</InfiniteScroll>
);
};
Expand Down
13 changes: 7 additions & 6 deletions webui/src/components/extension-searchfield.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,11 @@
********************************************************************************/

import { ChangeEvent, ForwardedRef, forwardRef, KeyboardEvent, useCallback, useId, useRef } from 'react';
import SearchIcon from '@mui/icons-material/Search';
import ClearIcon from '@mui/icons-material/Close';
import { IconButton, InputBase, InputBaseComponentProps, Box } from '@mui/material';
import { alpha, styled } from '@mui/material/styles';
import { MONO_FONT } from '../default/theme';
import SearchIcon from '@mui/icons-material/Search';
import ClearIcon from '@mui/icons-material/Close';
import { focusRing } from './page-primitives';

interface ExtensionSearchfieldProps {
Expand All @@ -40,8 +40,8 @@ const SearchWrap = styled(Box, {
gap: '0.625rem',
border: hasError ? '2px solid' : '1px solid',
borderColor: hasError ? theme.palette.error.main : theme.palette.divider,
borderRadius: '11px',
height: '2.8125rem',
borderRadius: theme.shape.borderRadiusCard,
height: '2.8rem',
padding: '0 0.8125rem',
backgroundColor: alpha(theme.palette.surface2, 0.7),
backdropFilter: 'blur(2px)',
Expand Down Expand Up @@ -154,8 +154,9 @@ export const ExtensionSearchfield = forwardRef(
sx={{
bgcolor: 'secondary.main',
color: 'secondary.contrastText',
borderRadius: '8px',
p: '0.5rem',
// sx multiplies by theme.shape.borderRadius
borderRadius: 1,
p: '0.375rem',
flexShrink: 0,
transition: 'background 0.14s',
'&:hover': { bgcolor: 'secondary.dark' }
Expand Down
2 changes: 1 addition & 1 deletion webui/src/components/kbd-key.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ export const KbdKey: FunctionComponent<PropsWithChildren> = ({ children }) => (
pb: '0.1875rem',
minWidth: '1.125rem',
textAlign: 'center',
borderRadius: '4px',
borderRadius: 1,
// All colors mix from the inherited text color, so the chip tints with
// its context instead of being a fixed gray.
color: 'color-mix(in srgb, currentcolor 62%, transparent)',
Expand Down
2 changes: 1 addition & 1 deletion webui/src/components/page-container.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ export const PageContainer = forwardRef<HTMLDivElement, PageContainerProps>(func
disableGutters={fluid}
sx={[
{
pt: flushTop ? 0 : { xs: '2.75rem', sm: '4.875rem' },
pt: flushTop ? 0 : { xs: '1.75rem', sm: '2.5rem' },
pb: flushBottom ? 0 : { xs: '2.5rem', sm: '4rem' }
},
...(Array.isArray(sx) ? sx : [sx])
Expand Down
Loading