From dfb89c32285c904720e9a5ca6212b60288d9b39d Mon Sep 17 00:00:00 2001 From: Tsahi Matsliah Date: Thu, 30 Jul 2026 09:53:19 +0300 Subject: [PATCH 1/5] fix(cards): drop the award action from feed cards when impressions are on Impressions take the award slot on the right of the engagement bar, so the two never coexist. Awards were only dropped below laptop; now they are gone on every viewport for grid, list and signal cards, in both engagement bar variants. With the card_impressions flag off, cards are unchanged. Also fixes the award button rendering for logged out visitors: `user?.id === post?.author?.id` compared undefined to undefined on authorless posts, so `isSameUser` was true and the button appeared on exactly the cards where an award is impossible. Co-Authored-By: Claude Opus 5 --- .../cards/common/ActionButtons.spec.tsx | 83 +++++++++++++++++++ .../components/cards/common/ActionButtons.tsx | 7 +- .../cards/common/ActionButtons.v2.tsx | 7 +- .../components/post/PostAwardAction.spec.tsx | 83 +++++++++++++++++++ .../src/components/post/PostAwardAction.tsx | 2 +- .../components/post/PostAwardAction.v2.tsx | 2 +- 6 files changed, 173 insertions(+), 11 deletions(-) create mode 100644 packages/shared/src/components/cards/common/ActionButtons.spec.tsx create mode 100644 packages/shared/src/components/post/PostAwardAction.spec.tsx diff --git a/packages/shared/src/components/cards/common/ActionButtons.spec.tsx b/packages/shared/src/components/cards/common/ActionButtons.spec.tsx new file mode 100644 index 00000000000..b14c847071d --- /dev/null +++ b/packages/shared/src/components/cards/common/ActionButtons.spec.tsx @@ -0,0 +1,83 @@ +import React from 'react'; +import { render, screen } from '@testing-library/react'; +import { QueryClient } from '@tanstack/react-query'; +import ActionButtons from './ActionButtons'; +import type { ActionButtonsVariant } from './ActionButtons'; +import post from '../../../../__tests__/fixture/post'; +import { TestBootProvider } from '../../../../__tests__/helpers/boot'; +import { usePostImpressions } from '../../../hooks/post/usePostImpressions'; +import { useEngagementBarV2 } from '../../../hooks/useEngagementBarV2'; +import { useViewSize } from '../../../hooks/useViewSize'; + +jest.mock('../../../hooks/post/usePostImpressions', () => ({ + usePostImpressions: jest.fn(), +})); + +// Awards used to survive on laptop when impressions were enabled, and jsdom +// reports every media query as unmatched, so the viewport has to be forced. +jest.mock('../../../hooks/useViewSize', () => ({ + ...jest.requireActual('../../../hooks/useViewSize'), + useViewSize: jest.fn(), +})); + +jest.mock('../../../hooks/post/usePostImpressionsModal', () => ({ + usePostImpressionsModal: () => jest.fn(), +})); + +jest.mock('../../../hooks/useEngagementBarV2', () => ({ + useEngagementBarV2: jest.fn(), +})); + +jest.mock('../../post/PostAwardAction', () => ({ + __esModule: true, + default: () =>
, +})); + +const mockImpressions = (enabled: boolean) => + jest.mocked(usePostImpressions).mockReturnValue({ + enabled, + showImpressions: enabled, + impressions: enabled ? 1000 : 0, + }); + +const renderComponent = (variant: ActionButtonsVariant) => + render( + + + , + ); + +const variants: ActionButtonsVariant[] = ['grid', 'list', 'signal']; + +describe.each([false, true])('ActionButtons (v2: %s)', (isV2) => { + beforeEach(() => { + jest.clearAllMocks(); + jest.mocked(useEngagementBarV2).mockReturnValue(isV2); + jest.mocked(useViewSize).mockReturnValue(true); + }); + + it.each(variants)( + 'hides the award action on a %s card when impressions are enabled', + (variant) => { + mockImpressions(true); + + renderComponent(variant); + + expect(screen.queryByTestId('award-action')).not.toBeInTheDocument(); + expect( + screen.getByRole('button', { name: 'Impressions' }), + ).toBeInTheDocument(); + }, + ); + + it.each(variants)( + 'keeps the award action on a %s card when impressions are disabled', + (variant) => { + mockImpressions(false); + + renderComponent(variant); + + expect(screen.getByTestId('award-action')).toBeInTheDocument(); + }, + ); +}); diff --git a/packages/shared/src/components/cards/common/ActionButtons.tsx b/packages/shared/src/components/cards/common/ActionButtons.tsx index effbd994982..c269236a608 100644 --- a/packages/shared/src/components/cards/common/ActionButtons.tsx +++ b/packages/shared/src/components/cards/common/ActionButtons.tsx @@ -250,10 +250,9 @@ const ActionButtonsV1 = ({ /> )} - {/* When impressions are enabled, drop awards below laptop to make room - for the extra action; with the flag off, awards stay on every - viewport (unchanged from control). */} - {showAwardAction && (!impressionsEnabled || isLaptop) && ( + {/* Impressions take the award slot on feed cards; with the flag off, + awards stay (unchanged from control). */} + {showAwardAction && !impressionsEnabled && ( )} { const config = variantConfig[variant]; const isFeedPreview = useFeedPreviewMode(); - // When impressions are enabled, awards are hidden below laptop (tablet + - // mobile) to make room for the extra action. - const isLaptop = useViewSize(ViewSize.Laptop); const { getUpvoteAnimation } = useBrandSponsorship(); const { @@ -207,7 +204,7 @@ const ActionButtons = ({ /> )} - {showAwardAction && (!impressionsEnabled || isLaptop) && ( + {showAwardAction && !impressionsEnabled && ( )} ({ + useAuthContext: jest.fn(), +})); + +jest.mock('../../hooks/useCoresFeature', () => ({ + useCanAwardUser: jest.fn(), +})); + +jest.mock('../../hooks/useEngagementBarV2', () => ({ + useEngagementBarV2: jest.fn(), +})); + +jest.mock('../../hooks/useLazyModal', () => ({ + useLazyModal: () => ({ openModal: jest.fn() }), +})); + +const post = { + id: 'p1', + numAwards: 0, +} as Post; + +const mockAuth = (user: Partial | null) => + jest + .mocked(useAuthContext) + .mockReturnValue({ user, showLogin: jest.fn() } as never); + +const renderComponent = (postProps: Partial = {}) => + render( + + + , + ); + +describe.each([false, true])('PostAwardAction (v2: %s)', (isV2) => { + beforeEach(() => { + jest.clearAllMocks(); + jest.mocked(useEngagementBarV2).mockReturnValue(isV2); + jest.mocked(useCanAwardUser).mockReturnValue(false); + }); + + it('stays hidden for a logged out user on a post without an author', () => { + mockAuth(null); + + renderComponent(); + + expect(screen.queryByRole('button')).not.toBeInTheDocument(); + }); + + it('stays hidden for a logged in user on a post without an author', () => { + mockAuth({ id: 'u1' }); + + renderComponent(); + + expect(screen.queryByRole('button')).not.toBeInTheDocument(); + }); + + it('renders on the logged in user own post', () => { + mockAuth({ id: 'u1' }); + + renderComponent({ author: { id: 'u1' } as Post['author'] }); + + expect(screen.getByRole('button')).toBeInTheDocument(); + }); + + it('renders when the author can be awarded', () => { + mockAuth({ id: 'u1' }); + jest.mocked(useCanAwardUser).mockReturnValue(true); + + renderComponent({ author: { id: 'u2' } as Post['author'] }); + + expect(screen.getByRole('button')).toBeInTheDocument(); + }); +}); diff --git a/packages/shared/src/components/post/PostAwardAction.tsx b/packages/shared/src/components/post/PostAwardAction.tsx index 34d876fb367..de01944830b 100644 --- a/packages/shared/src/components/post/PostAwardAction.tsx +++ b/packages/shared/src/components/post/PostAwardAction.tsx @@ -26,7 +26,7 @@ export interface PostAwardActionProps { const PostAwardActionV1 = ({ post, iconSize }: PostAwardActionProps) => { const { openModal } = useLazyModal(); const { user, showLogin } = useAuthContext(); - const isSameUser = user?.id === post?.author?.id; + const isSameUser = !!user?.id && user.id === post?.author?.id; const canAward = useCanAwardUser({ sendingUser: user, receivingUser: post?.author as LoggedUser, diff --git a/packages/shared/src/components/post/PostAwardAction.v2.tsx b/packages/shared/src/components/post/PostAwardAction.v2.tsx index 8c6ec1c4e66..e8368d873fb 100644 --- a/packages/shared/src/components/post/PostAwardAction.v2.tsx +++ b/packages/shared/src/components/post/PostAwardAction.v2.tsx @@ -27,7 +27,7 @@ const PostAwardAction = ({ }: PostAwardActionProps) => { const { openModal } = useLazyModal(); const { user, showLogin } = useAuthContext(); - const isSameUser = user?.id === post?.author?.id; + const isSameUser = !!user?.id && user.id === post?.author?.id; const canAward = useCanAwardUser({ sendingUser: user, receivingUser: post?.author as LoggedUser, From 9ff9afdf8979b3a54c60e3f5ee7388d371fa4775 Mon Sep 17 00:00:00 2001 From: Tsahi Matsliah Date: Thu, 30 Jul 2026 10:19:22 +0300 Subject: [PATCH 2/5] fix(cards): match the default card action bar to the floating glass bar MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The impressions counter sat 5px from the card edge on the default bar, so the number read as touching the rounded corner. Adopt the sizing the glass pill already uses (FeedCardGlassActions, #6260): XSmall buttons, 16px icons, `!pl-0.5 pr-0.5` around counters and asymmetric `pl-1 pr-2.5` row padding, which gives the two bars identical internal geometry — first icon 9px in, trailing number 13px from the edge, verified at the 272px min card width. Drops the mobile-only `typo-caption1` counter: the count is now footnote everywhere, matching the glass bar. Co-Authored-By: Claude Opus 5 --- .../components/cards/common/ActionButtons.tsx | 41 ++++++++++--------- 1 file changed, 22 insertions(+), 19 deletions(-) diff --git a/packages/shared/src/components/cards/common/ActionButtons.tsx b/packages/shared/src/components/cards/common/ActionButtons.tsx index c269236a608..d15c8032f14 100644 --- a/packages/shared/src/components/cards/common/ActionButtons.tsx +++ b/packages/shared/src/components/cards/common/ActionButtons.tsx @@ -11,7 +11,7 @@ import { DownvoteIcon, } from '../../icons'; import { ButtonColor, ButtonSize, ButtonVariant } from '../../buttons/Button'; -import { useFeedPreviewMode, useViewSize, ViewSize } from '../../../hooks'; +import { useFeedPreviewMode } from '../../../hooks'; import { UpvoteButtonIcon } from './UpvoteButtonIcon'; import { BookmarkButton } from '../../buttons'; import { IconSize } from '../../Icon'; @@ -43,30 +43,40 @@ export interface ActionButtonsProps { showAwardAction?: boolean; } +// Sizing follows the floating glass bar (FeedCardGlassActions): XSmall buttons +// with 16px icons, so six actions incl. the impressions counter fit a 272px +// card without the trailing number reaching the edge. const variantConfig = { grid: { - buttonSize: ButtonSize.Small, - iconSize: IconSize.XSmall, - containerClassName: 'px-1 pb-1', + buttonSize: ButtonSize.XSmall, + iconSize: IconSize.Size16, + // Asymmetric like the glass pill: the left edge holds an icon, the right + // edge holds the impressions number, which needs more room to sit right. + containerClassName: 'pb-1 pl-1 pr-2.5', showTagsPanel: false, useCommentLink: false, }, list: { - buttonSize: ButtonSize.Small, - iconSize: IconSize.XSmall, + buttonSize: ButtonSize.XSmall, + iconSize: IconSize.Size16, containerClassName: '', showTagsPanel: true, useCommentLink: true, }, signal: { - buttonSize: ButtonSize.Small, - iconSize: IconSize.XSmall, + buttonSize: ButtonSize.XSmall, + iconSize: IconSize.Size16, containerClassName: '', showTagsPanel: false, useCommentLink: true, }, } as const; +// Monospaced digits so counters never jitter, and a hair of padding on each +// side of the number — both taken from the glass bar. +const counterClassName = 'tabular-nums typo-footnote'; +const counterLabelClassName = '!pl-0.5 pr-0.5'; + const ActionButtonsV1 = ({ post, onUpvoteClick, @@ -81,14 +91,7 @@ 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 { @@ -145,7 +148,7 @@ const ActionButtonsV1 = ({ href={post.commentsPermalink} > } pressed={post.commented} @@ -206,7 +209,7 @@ const ActionButtonsV1 = ({ side={variant === 'grid' ? 'bottom' : undefined} > } From 6df638be4eec29dc82ba8133fe53ca278f876258 Mon Sep 17 00:00:00 2001 From: Tsahi Matsliah Date: Thu, 30 Jul 2026 10:33:04 +0300 Subject: [PATCH 3/5] fix(cards): keep the feed action bar inside the 272px min card width MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The v2 bar pushed its trailing action up to 41px outside the card on the narrowest grid track. Buttons never shrink here (global flex-shrink: 0), so six actions plus counters have to fit on intrinsic widths alone, and at Small size (32px buttons, 20px icons) they need up to 288px inside a 262px row. Two changes close the gap: - new `tight` CardAction density (XSmall buttons, 16px icons), used by the feed bar only, so v2 matches the v1 and glass bars. - no `gap` on the `feedCard` CardActionBar layout: `justify-between` already spreads the actions when there is slack, and the gap only added 20px the card could not give back. Both default bars now sit at a 36px row height (`py-1.5` around the h-6 buttons) so shrinking the actions doesn't make the bar read as cramped. Verified at 272px and 320px with 36·3·52.4K, 100·80·100K, 200·80·234.5K and 10K·999·1.2M: nothing escapes its container in any combination. Co-Authored-By: Claude Opus 5 --- packages/shared/src/components/buttons/CardAction.tsx | 7 ++++++- packages/shared/src/components/buttons/CardActionBar.tsx | 5 ++++- .../shared/src/components/cards/common/ActionButtons.tsx | 5 ++++- .../src/components/cards/common/ActionButtons.v2.tsx | 7 +++++-- packages/shared/src/components/post/PostAwardAction.tsx | 3 ++- packages/webapp/pages/dev/buttons.tsx | 2 +- 6 files changed, 22 insertions(+), 7 deletions(-) diff --git a/packages/shared/src/components/buttons/CardAction.tsx b/packages/shared/src/components/buttons/CardAction.tsx index f5e64df8b7d..70309415c5f 100644 --- a/packages/shared/src/components/buttons/CardAction.tsx +++ b/packages/shared/src/components/buttons/CardAction.tsx @@ -14,18 +14,23 @@ import { ButtonSize, ButtonVariant, ButtonIconPosition } from './common'; import type { ColorName } from '../../styles/colors'; import InteractionCounter from '../InteractionCounter'; -export type CardActionDensity = 'comfortable' | 'compact'; +export type CardActionDensity = 'comfortable' | 'compact' | 'tight'; const densityToSize: Record = { comfortable: ButtonSize.Medium, compact: ButtonSize.Small, + tight: ButtonSize.XSmall, }; // Larger than buttonSizeToIconSizeV2: engagement-bar icons sit closer // to a 60% ratio (Material 3, Instagram, Reddit) so they read at a glance. +// `tight` is the feed-card tier: buttons never shrink (global flex-shrink: 0), +// so six actions plus counters have to fit the 272px min card width on their +// intrinsic widths alone. It matches the floating glass bar. const densityToIconSize: Record = { comfortable: IconSize.Small, compact: IconSize.XSmall, + tight: IconSize.Size16, }; type IconElement = React.ReactElement; diff --git a/packages/shared/src/components/buttons/CardActionBar.tsx b/packages/shared/src/components/buttons/CardActionBar.tsx index 6b0a9233a43..d03a55ba766 100644 --- a/packages/shared/src/components/buttons/CardActionBar.tsx +++ b/packages/shared/src/components/buttons/CardActionBar.tsx @@ -10,7 +10,10 @@ export type CardActionBarLayout = const layoutToClass: Record = { default: 'gap-1', - feedCard: 'flex-1 min-w-0 gap-1 justify-between', + // No `gap` on the feed row: `justify-between` already spreads the actions + // whenever there is slack, and because buttons never shrink (global + // flex-shrink: 0) a gap only adds width the 272px min card cannot give back. + feedCard: 'flex-1 min-w-0 justify-between', between: 'gap-1 justify-between w-full', compact: 'gap-0.5', }; diff --git a/packages/shared/src/components/cards/common/ActionButtons.tsx b/packages/shared/src/components/cards/common/ActionButtons.tsx index d15c8032f14..d324d43f6d4 100644 --- a/packages/shared/src/components/cards/common/ActionButtons.tsx +++ b/packages/shared/src/components/cards/common/ActionButtons.tsx @@ -52,7 +52,10 @@ const variantConfig = { iconSize: IconSize.Size16, // Asymmetric like the glass pill: the left edge holds an icon, the right // edge holds the impressions number, which needs more room to sit right. - containerClassName: 'pb-1 pl-1 pr-2.5', + // `py-1.5` around the h-6 buttons gives the row the same 36px height the + // bar had at the previous (wider) button size, so shrinking the actions to + // fit the 272px card doesn't make the bar read as cramped vertically. + containerClassName: 'py-1.5 pl-1 pr-2.5', showTagsPanel: false, useCommentLink: false, }, diff --git a/packages/shared/src/components/cards/common/ActionButtons.v2.tsx b/packages/shared/src/components/cards/common/ActionButtons.v2.tsx index 0d48c220896..d5ea8c33091 100644 --- a/packages/shared/src/components/cards/common/ActionButtons.v2.tsx +++ b/packages/shared/src/components/cards/common/ActionButtons.v2.tsx @@ -39,11 +39,14 @@ export interface ActionButtonsProps { showAwardAction?: boolean; } -const FEED_CARD_DENSITY = 'compact'; +const FEED_CARD_DENSITY = 'tight'; const variantConfig = { grid: { - containerClassName: 'px-1 pb-1', + // Same padding as the v1 bar and the glass pill: `py-1.5` keeps the row at + // 36px around the h-6 buttons, and the wider right edge gives the trailing + // impressions number room so it doesn't read as touching the card. + containerClassName: 'py-1.5 pl-1 pr-2.5', showTagsPanel: false, useCommentLink: false, }, diff --git a/packages/shared/src/components/post/PostAwardAction.tsx b/packages/shared/src/components/post/PostAwardAction.tsx index de01944830b..48d5a815d6b 100644 --- a/packages/shared/src/components/post/PostAwardAction.tsx +++ b/packages/shared/src/components/post/PostAwardAction.tsx @@ -15,12 +15,13 @@ import { AuthTriggers } from '../../lib/auth'; import { LazyModal } from '../modals/common/types'; import type { LoggedUser } from '../../lib/user'; import { useEngagementBarV2 } from '../../hooks/useEngagementBarV2'; +import type { CardActionDensity } from '../buttons/CardAction'; import PostAwardActionV2 from './PostAwardAction.v2'; export interface PostAwardActionProps { post: Post; iconSize?: IconSize; - density?: 'comfortable' | 'compact'; + density?: CardActionDensity; } const PostAwardActionV1 = ({ post, iconSize }: PostAwardActionProps) => { diff --git a/packages/webapp/pages/dev/buttons.tsx b/packages/webapp/pages/dev/buttons.tsx index 0b9b222e59a..a0da609c2ac 100644 --- a/packages/webapp/pages/dev/buttons.tsx +++ b/packages/webapp/pages/dev/buttons.tsx @@ -1389,7 +1389,7 @@ const ButtonsDevPage = (): ReactElement => {
Date: Thu, 30 Jul 2026 10:34:56 +0300 Subject: [PATCH 4/5] chore(storybook): action bar alignment comparison page Renders v1 / v2 / glass feed bars at 320px and the 272px min card width across four stat loads, so the width contract stays checkable. Co-Authored-By: Claude Opus 5 --- .../cards/ActionBarAlignment.stories.tsx | 132 ++++++++++++++++++ 1 file changed, 132 insertions(+) create mode 100644 packages/storybook/stories/components/cards/ActionBarAlignment.stories.tsx diff --git a/packages/storybook/stories/components/cards/ActionBarAlignment.stories.tsx b/packages/storybook/stories/components/cards/ActionBarAlignment.stories.tsx new file mode 100644 index 00000000000..3c852d535db --- /dev/null +++ b/packages/storybook/stories/components/cards/ActionBarAlignment.stories.tsx @@ -0,0 +1,132 @@ +import type { Meta, StoryObj } from '@storybook/react-vite'; +import React from 'react'; +import { fn } from 'storybook/test'; +import type { Post } from '@dailydotdev/shared/src/graphql/posts'; +import { PostType, UserVote } from '@dailydotdev/shared/src/graphql/posts'; +import { ArticleGrid } from '@dailydotdev/shared/src/components/cards/article/ArticleGrid'; +import ExtensionProviders from '../../extension/_providers'; +import { FeatureOverrides } from '../../../mock/GrowthBookProvider'; + +const basePost = { + id: 'article-1', + title: + 'Cloud Run now scales to zero, even when your service uses less than a single CPU or less than 1792 MB of memory', + permalink: 'https://api.daily.dev/r/article-1', + commentsPermalink: 'https://daily.dev/posts/article-1', + createdAt: '2024-01-15T10:30:00.000Z', + readTime: 8, + tags: ['javascript'], + type: PostType.Article, + image: + 'https://media.daily.dev/image/upload/f_auto,q_auto/v1/posts/article-placeholder', + userState: { vote: UserVote.None, flags: { feedbackDismiss: false } }, + source: { + id: 'tds', + handle: 'tds', + name: 'Towards Data Science', + permalink: 'https://app.daily.dev/sources/tds', + image: 'https://media.daily.dev/image/upload/t_logo,f_auto/v1/logos/tds', + type: 'machine' as const, + active: true, + }, +} as unknown as Post; + +const makePost = ( + id: string, + numUpvotes: number, + numComments: number, + impressions: number, +): Post => + ({ + ...basePost, + id, + numUpvotes, + numComments, + analytics: { impressions }, + } as Post); + +const cases = [ + { label: '36 · 3 · 52.4K', post: makePost('a', 36, 3, 52400) }, + { label: '100 · 80 · 100K', post: makePost('b', 100, 80, 100000) }, + { label: '200 · 80 · 200K', post: makePost('c', 200, 80, 234500) }, + { label: '9999 · 999 · 1.2M', post: makePost('d', 9999, 999, 1200000) }, +]; + +const handlers = { + onPostClick: fn(), + onPostAuxClick: fn(), + onUpvoteClick: fn(), + onDownvoteClick: fn(), + onCommentClick: fn(), + onBookmarkClick: fn(), + onCopyLinkClick: fn(), + onShare: fn(), + onReadArticleClick: fn(), +}; + +const v1 = { + card_impressions: true, + engagement_bar_v2: false, + feed_card_glass_actions: false, +}; +const v2 = { ...v1, engagement_bar_v2: true }; +const glass = { ...v1, feed_card_glass_actions: true }; + +const Row = ({ + title, + values, + width, +}: { + title: string; + values: Record; + width: string; +}) => ( +
+

{title}

+
+ {cases.map(({ label, post }) => ( +
+

{label}

+ + + +
+ ))} +
+
+); + +const ActionBarAlignment = () => ( + +
+ + + + + + +
+
+); + +const meta: Meta = { + title: 'Components/Cards/ActionBarAlignment', + component: ActionBarAlignment, + parameters: { layout: 'fullscreen' }, +}; + +export default meta; + +export const Default: StoryObj = {}; From 9da44157f97a6f2eabc2cbfa9156d4b3d7d0b25f Mon Sep 17 00:00:00 2001 From: Tsahi Matsliah Date: Thu, 30 Jul 2026 11:00:48 +0300 Subject: [PATCH 5/5] chore(dev): move the buttons playground feed vignettes to the tight density The /dev/buttons width-contract vignettes documented `density="compact"` as the grid-card rule, which overflows the 272px MIN once a card carries six actions with counters. They now use `tight`, matching what the feed ships. Co-Authored-By: Claude Opus 5 --- packages/webapp/pages/dev/buttons.tsx | 64 +++++++++++++-------------- 1 file changed, 32 insertions(+), 32 deletions(-) diff --git a/packages/webapp/pages/dev/buttons.tsx b/packages/webapp/pages/dev/buttons.tsx index a0da609c2ac..74e63ff36ce 100644 --- a/packages/webapp/pages/dev/buttons.tsx +++ b/packages/webapp/pages/dev/buttons.tsx @@ -621,14 +621,14 @@ const V1EngagementBar = ({ pressed = false }: { pressed?: boolean }) => ( ); /** - * v2 engagement bar — `density="compact"` + `feedCard` layout, the - * production rule for grid cards. Width footprint matches v1 - * exactly (5 × 32 px), so swapping in is layout-neutral. + * v2 engagement bar — `density="tight"` + `feedCard` layout, the + * production rule for grid cards. 24 px buttons with 16 px icons, + * matching the v1 bar and the floating glass pill. */ const V2EngagementBar = ({ pressed = false }: { pressed?: boolean }) => ( } iconPressed={} @@ -637,28 +637,28 @@ const V2EngagementBar = ({ pressed = false }: { pressed?: boolean }) => ( count={1234} /> } iconPressed={} label="Downvote" color={ButtonColor.Ketchup} /> } label="Comment" color={ButtonColor.BlueCheese} count={42} /> } iconPressed={} label="Bookmark" color={ButtonColor.Bun} /> } label="Copy link" color={ButtonColor.Cabbage} @@ -1141,7 +1141,7 @@ const ButtonsDevPage = (): ReactElement => { Subtle = outlined chip, Material 3 outlined pattern).{' '} Card-action width contract: on a multi-column feed grid (where a card can render at 140 – 280 px), use{' '} - density="compact" on every{' '} + density="tight" on every{' '} CardAction and wrap the row in{' '} <CardActionBar layout="feedCard"> — the bar then sits at flex-1 min-w-0 justify-between{' '} @@ -1432,27 +1432,27 @@ const ButtonsDevPage = (): ReactElement => {
} iconPressed={} label="Upvote" color={ButtonColor.Avocado} /> } label="Comment" color={ButtonColor.BlueCheese} /> } iconPressed={} label="Bookmark" color={ButtonColor.Bun} /> } label="Share" /> @@ -1506,7 +1506,7 @@ const ButtonsDevPage = (): ReactElement => {
} iconPressed={} label="Upvote" @@ -1514,21 +1514,21 @@ const ButtonsDevPage = (): ReactElement => { count={1234} /> } label="Comment" color={ButtonColor.BlueCheese} count={42} /> } iconPressed={} label="Bookmark" color={ButtonColor.Bun} /> } label="Share" /> @@ -1584,7 +1584,7 @@ const ButtonsDevPage = (): ReactElement => {
} iconPressed={} @@ -1593,14 +1593,14 @@ const ButtonsDevPage = (): ReactElement => { count={1235} /> } label="Comment" color={ButtonColor.BlueCheese} count={42} /> } iconPressed={} @@ -1608,7 +1608,7 @@ const ButtonsDevPage = (): ReactElement => { color={ButtonColor.Bun} /> } label="Share" /> @@ -1685,34 +1685,34 @@ const ButtonsDevPage = (): ReactElement => {
} iconPressed={} label="Upvote" color={ButtonColor.Avocado} /> } iconPressed={} label="Downvote" color={ButtonColor.Ketchup} /> } label="Comment" color={ButtonColor.BlueCheese} /> } iconPressed={} label="Bookmark" color={ButtonColor.Bun} /> } label="Share" /> @@ -1773,13 +1773,13 @@ const ButtonsDevPage = (): ReactElement => {
- RIGHT @ 272 px · feedCard layout + compact density — bar + RIGHT @ 272 px · feedCard layout + tight density — bar fills 272 px, distributes children, never pushes the card
} iconPressed={} label="Upvote" @@ -1787,28 +1787,28 @@ const ButtonsDevPage = (): ReactElement => { count={1234} /> } iconPressed={} label="Downvote" color={ButtonColor.Ketchup} /> } label="Comment" color={ButtonColor.BlueCheese} count={42} /> } iconPressed={} label="Bookmark" color={ButtonColor.Bun} /> } label="Share" />