Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
a0a48bd
fix(cards): even glass action bar spacing, no count clipping, no hero…
tsahimatsliah Jun 23, 2026
cdd2839
Merge branch 'main' into claude/zen-borg-db678b
tsahimatsliah Jun 23, 2026
972d9dc
fix(cards): shrink hero title to fit three lines so the TLDR stays vi…
tsahimatsliah Jun 23, 2026
6a09438
style(cards): equal glass bar padding on all sides, slightly darker fill
tsahimatsliah Jun 23, 2026
9e34b03
fix(cards): restore 3-line title clamp on glass article cards
tsahimatsliah Jun 23, 2026
6e582fa
feat(cards): add impressions action to the feed card glass bar
tsahimatsliah Jun 23, 2026
e7ef2f2
feat(cards): show impressions everywhere, shrink glass bar, add to po…
tsahimatsliah Jun 23, 2026
1709c03
style(post): shrink post action bar buttons to match the compact feed…
tsahimatsliah Jun 23, 2026
8393d92
Merge remote-tracking branch 'origin/main' into claude/feed-card-impr…
tsahimatsliah Jun 24, 2026
099dd5d
fix(cards): taller glass bar + always-visible impressions count
tsahimatsliah Jun 24, 2026
23798e4
feat(cards): show post impressions with a mock fallback for review
tsahimatsliah Jun 24, 2026
ed3b2ee
revert(post): keep post page/modal action bars at their original size
tsahimatsliah Jun 24, 2026
02d6157
style(buttons): default QuaternaryButton label to font-medium per gui…
tsahimatsliah Jun 24, 2026
b3b7a2e
style(cards): move impressions/analytics action to the end of the bar
tsahimatsliah Jun 24, 2026
505bc11
feat(cards): X/Twitter-style impressions number (decimal only under 10)
tsahimatsliah Jun 24, 2026
da22ebb
feat(cards): impressions explainer popup + optical right padding
tsahimatsliah Jun 24, 2026
fafd1e9
Merge branch 'main' into claude/feed-card-impressions
tsahimatsliah Jun 24, 2026
bbff68d
fix(cards): wire impressions click for owners/team → analytics, other…
tsahimatsliah Jun 24, 2026
ec94dc9
feat(cards): add impressions to the standard action bar (list + all r…
tsahimatsliah Jun 24, 2026
ad8032d
style(cards): compact action bar on small screens + downvote 3rd to m…
tsahimatsliah Jun 24, 2026
69e1b29
fix(cards): hide feed awards on tablet/mobile + consistent impression…
tsahimatsliah Jun 24, 2026
c5d8ec4
fix(cards): impressions number turns yellow on hover like the other s…
tsahimatsliah Jun 24, 2026
0ba1a5f
docs(posts): correct the views field comment (also in SharedPostInfo)
tsahimatsliah Jun 24, 2026
7b7f34d
style(cards): bigger icons + smaller stat numbers on mobile/tablet fe…
tsahimatsliah Jun 24, 2026
2be0671
feat(post): impressions stat next to comments; move it left in the st…
tsahimatsliah Jun 24, 2026
c220f82
feat(post): impressions live in the stats strip, not the post action bar
tsahimatsliah Jun 24, 2026
415f926
fix(post): always show impressions in the stats strip via mock fallback
tsahimatsliah Jun 24, 2026
582b6c2
fix(cards): vertically center the stat count in its fixed-height box
tsahimatsliah Jun 24, 2026
663b3ae
fix(post): prefer real impressions over the mock in the stats strip
tsahimatsliah Jun 24, 2026
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
27 changes: 19 additions & 8 deletions packages/shared/src/components/InteractionCounter.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,20 +7,23 @@ import { largeNumberFormat } from '../lib';
export type InteractionCounterProps = {
className?: string;
value: number | null;
/** Override the number formatter (defaults to `largeNumberFormat`). */
format?: (value: number | null) => string | null;
};

export default function InteractionCounter({
className,
value,
format = largeNumberFormat,
...props
}: InteractionCounterProps): ReactElement {
const [shownValue, setShownValue] = useState(value);
const [animate, setAnimate] = useState(false);
useEffect(() => {
const formattedValue = largeNumberFormat(value);
const formattedShownValue = largeNumberFormat(shownValue);
const formattedValue = format(value);
const formattedShownValue = format(shownValue);
if (formattedValue !== formattedShownValue) {
if (value < shownValue) {
if ((value ?? 0) < (shownValue ?? 0)) {
setShownValue(value);
} else {
setAnimate(false);
Expand All @@ -37,9 +40,15 @@ export default function InteractionCounter({
);

if (shownValue === value) {
// Center the number within the fixed-height (h-5) box. Without this the text
// is top-aligned, so smaller type (e.g. typo-caption1 on mobile/tablet, whose
// 1rem line-height is shorter than h-5) sits visibly higher than the icon.
return (
<span className={elementClassName} {...props}>
{largeNumberFormat(shownValue)}
<span
className={classNames(elementClassName, 'justify-center')}
{...props}
>
{format(shownValue)}
</span>
);
}
Expand All @@ -49,8 +58,10 @@ export default function InteractionCounter({
setShownValue(value);
};

// leading-5 makes each rolling slice's line box fill its h-5 height so the
// digits stay centered during the roll (matches the resting state above).
const childClassName =
'h-5 inline-block transition-[opacity,transform] ease-in-out duration-300 will-change-[opacity,transform]';
'h-5 leading-5 inline-block transition-[opacity,transform] ease-in-out duration-300 will-change-[opacity,transform]';

return (
<span className={elementClassName} {...props}>
Expand All @@ -60,7 +71,7 @@ export default function InteractionCounter({
animate ? '-translate-y-full opacity-0' : 'translate-y-0 opacity-100',
)}
>
{largeNumberFormat(shownValue)}
{format(shownValue)}
</span>
<span
className={classNames(
Expand All @@ -69,7 +80,7 @@ export default function InteractionCounter({
)}
onTransitionEnd={updateShownValue}
>
{largeNumberFormat(value)}
{format(value)}
</span>
</span>
);
Expand Down
7 changes: 6 additions & 1 deletion packages/shared/src/components/buttons/CardAction.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,8 @@ type CardActionBaseProps = CardActionPassthroughProps & {
iconPressed?: IconElement;
label: string;
count?: number | null;
/** Override the counter formatter (defaults to `largeNumberFormat`). */
countFormat?: (value: number | null) => string | null;
color?: ColorName;
pressed?: boolean;
loading?: boolean;
Expand All @@ -64,6 +66,7 @@ function CardActionComponent(
iconPressed,
label,
count,
countFormat,
color,
pressed,
loading,
Expand Down Expand Up @@ -121,7 +124,9 @@ function CardActionComponent(
{showLabel && (
<span className="card-action-label truncate">{label}</span>
)}
{showCount && <InteractionCounter value={count ?? 0} />}
{showCount && (
<InteractionCounter value={count ?? 0} format={countFormat} />
)}
</span>
)}
</ButtonV2>
Expand Down
4 changes: 3 additions & 1 deletion packages/shared/src/components/buttons/QuaternaryButton.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,9 @@ function QuaternaryButtonComponent<TagName extends AllowedTags>(
htmlFor={id}
{...labelProps}
className={classNames(
'flex cursor-pointer items-center pl-1 font-bold typo-callout',
// Medium weight to match the button typography guideline (the
// Button base and CardAction both use font-medium).
'flex cursor-pointer items-center pl-1 font-medium typo-callout',
{ readOnly: props.disabled },
labelClassName,
)}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import { FeedCardGlassActions } from '../common/FeedCardGlassActions';
import { FeedbackGrid } from './feedback/FeedbackGrid';
import { ClickbaitShield } from '../common/ClickbaitShield';
import { useSmartTitle } from '../../../hooks/post/useSmartTitle';
import { useFitFontSize } from '../../../hooks/useFitFontSize';
import { useFeedCardGlassActions } from '../../../hooks/useFeedCardGlassActions';
import { usePostImage } from '../../../hooks/post/usePostImage';
import { useCardCover } from '../../../hooks/feed/useCardCover';
Expand All @@ -51,6 +52,12 @@ const IMAGE_COL_SPAN: Record<FeaturedWideColSpan, string> = {
4: 'col-span-3',
};

// The hero title shrinks (title1 → title3) to stay within three lines so the
// summary below always has room, rather than spilling onto a fourth line and
// pushing the TLDR off the card.
const HERO_TITLE_SIZE_CLASSES = ['typo-title1', 'typo-title2', 'typo-title3'];
const HERO_TITLE_MAX_LINES = 3;

const HighlightChip = ({
significance,
className,
Expand Down Expand Up @@ -148,6 +155,15 @@ export const ArticleFeaturedWideGridCard = forwardRef(
const { pinnedAt } = post;
const { showFeedback } = usePostFeedback({ post });
const { title } = useSmartTitle(post);
const {
ref: titleRef,
sizeClass: titleSizeClass,
isClamped: titleClamped,
} = useFitFontSize<HTMLHeadingElement>({
text: title,
sizeClasses: HERO_TITLE_SIZE_CLASSES,
maxLines: HERO_TITLE_MAX_LINES,
});
const isVideoType = isVideoPost(post);
const image = usePostImage(post);
const { overlay } = useCardCover({ post, onShare });
Expand Down Expand Up @@ -276,9 +292,11 @@ export const ArticleFeaturedWideGridCard = forwardRef(
showFeedback={false}
/>
<h3
ref={titleRef}
className={classNames(
'mt-2 break-words font-bold text-text-primary typo-title1',
useGlass ? 'line-clamp-3' : 'line-clamp-4',
'mt-2 break-words font-bold text-text-primary',
titleSizeClass,
titleClamped && 'line-clamp-3',
)}
>
{title}
Expand Down
87 changes: 62 additions & 25 deletions packages/shared/src/components/cards/common/ActionButtons.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,13 @@ import type { Post } from '../../../graphql/posts';
import InteractionCounter from '../../InteractionCounter';
import { QuaternaryButton } from '../../buttons/QuaternaryButton';
import {
AnalyticsIcon,
DiscussIcon as CommentIcon,
LinkIcon,
DownvoteIcon,
} from '../../icons';
import { ButtonColor, ButtonSize, ButtonVariant } from '../../buttons/Button';
import { useFeedPreviewMode } from '../../../hooks';
import { useFeedPreviewMode, useViewSize, ViewSize } from '../../../hooks';
import { UpvoteButtonIcon } from './UpvoteButtonIcon';
import { BookmarkButton } from '../../buttons';
import { IconSize } from '../../Icon';
Expand All @@ -21,6 +22,11 @@ import { PostTagsPanel } from '../../post/block/PostTagsPanel';
import { LinkWithTooltip } from '../../tooltips/LinkWithTooltip';
import { useCardActions } from '../../../hooks/cards/useCardActions';
import { useBrandSponsorship } from '../../../hooks/useBrandSponsorship';
import { usePostImpressionsModal } from '../../../hooks/post/usePostImpressionsModal';
import {
formatImpressions,
getPostImpressions,
} from '../../../lib/impressions';
import { useEngagementBarV2 } from '../../../hooks/useEngagementBarV2';
import ActionButtonsV2 from './ActionButtons.v2';

Expand Down Expand Up @@ -78,6 +84,14 @@ const ActionButtonsV1 = ({
}: ActionButtonsProps): ReactElement | null => {
const config = variantConfig[variant];
const isFeedPreview = useFeedPreviewMode();
const isLaptop = useViewSize(ViewSize.Laptop);
const { buttonSize, iconSize } = config;
// On mobile/tablet keep full-size icons but shrink the count so the icon
// reads as the primary affordance and the number as a subtle stat.
const counterClassName = classNames(
'tabular-nums',
isLaptop ? variant === 'grid' && 'typo-footnote' : 'typo-caption1',
);
const { getUpvoteAnimation } = useBrandSponsorship();

const {
Expand Down Expand Up @@ -114,6 +128,8 @@ const ActionButtonsV1 = ({
};
}, [getUpvoteAnimation, post.tags]);

const onImpressionsClick = usePostImpressionsModal(post);

if (isFeedPreview) {
return null;
}
Expand All @@ -135,13 +151,16 @@ const ActionButtonsV1 = ({
href={post.commentsPermalink}
pressed={post.commented}
variant={ButtonVariant.Tertiary}
size={config.buttonSize}
icon={<CommentIcon secondary={post.commented} size={config.iconSize} />}
size={buttonSize}
icon={<CommentIcon secondary={post.commented} size={iconSize} />}
onClick={() => onCommentClick?.(post)}
>
{commentCount > 0 && (
<InteractionCounter
className={classNames('tabular-nums', !commentCount && 'invisible')}
className={classNames(
counterClassName,
!commentCount && 'invisible',
)}
value={commentCount}
/>
)}
Expand All @@ -152,16 +171,16 @@ const ActionButtonsV1 = ({
<QuaternaryButton
labelClassName="!pl-[1px]"
id={`post-${post.id}-comment-btn`}
icon={<CommentIcon secondary={post.commented} size={config.iconSize} />}
icon={<CommentIcon secondary={post.commented} size={iconSize} />}
pressed={post.commented}
onClick={() => onCommentClick?.(post)}
size={config.buttonSize}
size={buttonSize}
className="btn-tertiary-blueCheese"
>
{commentCount > 0 && (
<InteractionCounter
className={classNames(
'tabular-nums !typo-footnote',
counterClassName,
!commentCount && 'invisible',
)}
value={commentCount}
Expand Down Expand Up @@ -192,26 +211,24 @@ const ActionButtonsV1 = ({
pressed={isUpvoteActive}
onClick={onToggleUpvote}
variant={ButtonVariant.Tertiary}
size={config.buttonSize}
size={buttonSize}
icon={
<UpvoteButtonIcon
secondary={isUpvoteActive}
size={config.iconSize}
size={iconSize}
brandAnimation={brandAnimation}
/>
}
>
{upvoteCount > 0 && (
<InteractionCounter
className={classNames(
'tabular-nums',
variant === 'grid' && 'typo-footnote',
)}
className={counterClassName}
value={upvoteCount}
/>
)}
</QuaternaryButton>
</Tooltip>
{commentButton}
{showDownvoteAction && (
<Tooltip
content={isDownvoteActive ? 'Remove downvote' : 'Downvote'}
Expand All @@ -222,29 +239,25 @@ const ActionButtonsV1 = ({
id={`post-${post.id}-downvote-btn`}
color={ButtonColor.Ketchup}
icon={
<DownvoteIcon
secondary={isDownvoteActive}
size={config.iconSize}
/>
<DownvoteIcon secondary={isDownvoteActive} size={iconSize} />
}
pressed={isDownvoteActive}
onClick={onToggleDownvote}
variant={ButtonVariant.Tertiary}
size={config.buttonSize}
size={buttonSize}
/>
</Tooltip>
)}
{commentButton}
{showAwardAction && (
<PostAwardAction post={post} iconSize={config.iconSize} />
{showAwardAction && isLaptop && (
<PostAwardAction post={post} iconSize={iconSize} />
)}
<BookmarkButton
tooltipSide={variant === 'grid' ? 'bottom' : undefined}
post={post}
buttonProps={{
id: `post-${post.id}-bookmark-btn`,
onClick: onToggleBookmark,
size: config.buttonSize,
size: buttonSize,
className: classNames(
'btn-tertiary-bun',
variant === 'list' && 'pointer-events-auto',
Expand All @@ -253,22 +266,46 @@ const ActionButtonsV1 = ({
variant: ButtonVariant.Tertiary,
}),
}}
iconSize={config.iconSize}
iconSize={iconSize}
/>
<Tooltip
content="Copy link"
side={variant === 'grid' ? 'bottom' : undefined}
>
<QuaternaryButton
id="copy-post-btn"
size={config.buttonSize}
icon={<LinkIcon size={config.iconSize} />}
size={buttonSize}
icon={<LinkIcon size={iconSize} />}
onClick={onCopyLink}
variant={ButtonVariant.Tertiary}
color={ButtonColor.Cabbage}
className={variant === 'list' ? 'pointer-events-auto' : undefined}
/>
</Tooltip>
<Tooltip
content="Impressions"
side={variant === 'grid' ? 'bottom' : undefined}
>
<QuaternaryButton
labelClassName={variant === 'grid' ? '!pl-[1px]' : '!pl-0'}
id={`post-${post.id}-impressions-btn`}
size={buttonSize}
icon={<AnalyticsIcon size={iconSize} />}
onClick={onImpressionsClick}
variant={ButtonVariant.Tertiary}
color={ButtonColor.Cheese}
className={classNames(
'btn-tertiary-cheese',
variant === 'list' && 'pointer-events-auto',
)}
>
<InteractionCounter
className={counterClassName}
value={getPostImpressions(post)}
format={formatImpressions}
/>
</QuaternaryButton>
</Tooltip>
</div>
</div>
);
Expand Down
Loading
Loading