From 60cff1b113cb7343fa3cef92fc283e2802f53498 Mon Sep 17 00:00:00 2001 From: Tsahi Matsliah Date: Sun, 28 Jun 2026 14:09:16 +0300 Subject: [PATCH 01/53] fix(post): make opening the source article an easy target The readability redesign shrank the cover image to a thumbnail and handed its click to a lightbox. That removed the big, learned target users relied on to open the original article, leaving only a small w-fit "Read" button. - Title now links to the source (shares the read button's reader gate) - Read button: Small -> Medium, full-width on mobile, open-link icon trails - Cover thumbnail gets a zoom badge so "tap to view image" reads clearly Co-Authored-By: Claude Opus 4.8 --- .../components/post/focus/PostFocusCard.tsx | 49 ++++++++++++++++--- 1 file changed, 42 insertions(+), 7 deletions(-) diff --git a/packages/shared/src/components/post/focus/PostFocusCard.tsx b/packages/shared/src/components/post/focus/PostFocusCard.tsx index 9180f39988..e13c3cf55c 100644 --- a/packages/shared/src/components/post/focus/PostFocusCard.tsx +++ b/packages/shared/src/components/post/focus/PostFocusCard.tsx @@ -18,7 +18,8 @@ import { useSmartTitle } from '../../../hooks/post/useSmartTitle'; import { useUpvoteQuery } from '../../../hooks/useUpvoteQuery'; import { useReaderInstallPromptGate } from '../../../hooks/useReaderInstallPromptGate'; import { useReaderModalEligibility } from '../reader/hooks/useReaderModalEligibility'; -import { EarthIcon } from '../../icons'; +import { EarthIcon, MaximizeIcon } from '../../icons'; +import { IconSize } from '../../Icon'; import { useLazyModal } from '../../../hooks/useLazyModal'; import { LazyModal } from '../../modals/common/types'; import { getImageOriginRect } from '../../modals/ImageModal'; @@ -28,7 +29,12 @@ import Markdown from '../../Markdown'; import { ContentEmbeds } from '../../contentEmbeds/ContentEmbeds'; import { LazyImage } from '../../LazyImage'; import { cloudinaryPostImageCoverPlaceholder } from '../../../lib/image'; -import { Button, ButtonSize, ButtonVariant } from '../../buttons/Button'; +import { + Button, + ButtonIconPosition, + ButtonSize, + ButtonVariant, +} from '../../buttons/Button'; import { getReadPostButtonIcon } from '../../cards/common/ReadArticleButton'; import { PostUpvotesCommentsCount } from '../PostUpvotesCommentsCount'; import { PostTagList } from '../tags/PostTagList'; @@ -252,6 +258,7 @@ export const PostFocusCard = ({ const videoWrapperRef = useRef(null); const [isVideoExpanded, setIsVideoExpanded] = useState(false); const readHref = getReadArticleHref(post); + const canReadArticle = !!readHref && !isInternalReadType(post); useEffect(() => { if (!isVideoType || isVideoExpanded) { @@ -295,16 +302,21 @@ export const PostFocusCard = ({ // to the title regardless of the cover image height. The engagement bar lives // further down by the comment composer where the reader's cursor rests. const renderReadButton = (className: string): ReactElement | null => - readHref && !isInternalReadType(post) ? ( + canReadArticle ? ( )} From c5eaa97705eb53c99ec55fa8b815896e75dc2384 Mon Sep 17 00:00:00 2001 From: Tsahi Matsliah Date: Sun, 28 Jun 2026 14:17:06 +0300 Subject: [PATCH 02/53] fix(post): drop cover image zoom badge Keep the read-discoverability changes (title link + promoted button) but remove the thumbnail zoom badge. Co-Authored-By: Claude Opus 4.8 --- .../src/components/post/focus/PostFocusCard.tsx | 13 ++----------- 1 file changed, 2 insertions(+), 11 deletions(-) diff --git a/packages/shared/src/components/post/focus/PostFocusCard.tsx b/packages/shared/src/components/post/focus/PostFocusCard.tsx index e13c3cf55c..1600de9f92 100644 --- a/packages/shared/src/components/post/focus/PostFocusCard.tsx +++ b/packages/shared/src/components/post/focus/PostFocusCard.tsx @@ -18,8 +18,7 @@ import { useSmartTitle } from '../../../hooks/post/useSmartTitle'; import { useUpvoteQuery } from '../../../hooks/useUpvoteQuery'; import { useReaderInstallPromptGate } from '../../../hooks/useReaderInstallPromptGate'; import { useReaderModalEligibility } from '../reader/hooks/useReaderModalEligibility'; -import { EarthIcon, MaximizeIcon } from '../../icons'; -import { IconSize } from '../../Icon'; +import { EarthIcon } from '../../icons'; import { useLazyModal } from '../../../hooks/useLazyModal'; import { LazyModal } from '../../modals/common/types'; import { getImageOriginRect } from '../../modals/ImageModal'; @@ -455,7 +454,7 @@ export const PostFocusCard = ({ )} From 2e07f159f8c20c1475ec7f4ee00ca19ce98b030d Mon Sep 17 00:00:00 2001 From: Tsahi Matsliah Date: Sun, 28 Jun 2026 14:19:36 +0300 Subject: [PATCH 03/53] fix(post): link author name to profile in focus card The source name already links via SourceStrip, but the author shown above the title (shared/freeform/welcome posts) rendered a dead anchor with no destination. Wrap UserShortInfo in a Link to author.permalink, mirroring the RepostListItem pattern. Co-Authored-By: Claude Opus 4.8 --- .../components/post/focus/PostFocusCard.tsx | 25 +++++++++++-------- 1 file changed, 15 insertions(+), 10 deletions(-) diff --git a/packages/shared/src/components/post/focus/PostFocusCard.tsx b/packages/shared/src/components/post/focus/PostFocusCard.tsx index 1600de9f92..11c173455f 100644 --- a/packages/shared/src/components/post/focus/PostFocusCard.tsx +++ b/packages/shared/src/components/post/focus/PostFocusCard.tsx @@ -332,16 +332,21 @@ export const PostFocusCard = ({
{author ? (
- null} - className={{ - container: 'min-w-0 !p-0 hover:bg-transparent', - textWrapper: 'min-w-0', - }} - /> + + null} + className={{ + container: + 'min-w-0 cursor-pointer !p-0 hover:bg-transparent', + textWrapper: 'min-w-0', + }} + /> + Date: Sun, 28 Jun 2026 14:20:22 +0300 Subject: [PATCH 04/53] fix(post): keep read button content-width on mobile Full-width on mobile stretched the button awkwardly relative to its label; match desktop and hug content at every breakpoint. Co-Authored-By: Claude Opus 4.8 --- packages/shared/src/components/post/focus/PostFocusCard.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/shared/src/components/post/focus/PostFocusCard.tsx b/packages/shared/src/components/post/focus/PostFocusCard.tsx index 11c173455f..3af8256f18 100644 --- a/packages/shared/src/components/post/focus/PostFocusCard.tsx +++ b/packages/shared/src/components/post/focus/PostFocusCard.tsx @@ -453,7 +453,7 @@ export const PostFocusCard = ({ title )} - {renderReadButton('w-full tablet:w-fit')} + {renderReadButton('w-fit')}
{!isVideoType && article.image && (
-
- {sharedVia && ( -

- Shared via - - - - {sharedVia.image && ( - - )} - {sharedVia.name} - - - - } - > - - -

- )} - {isShared && !sharedVia && ( -

Shared post

- )} - {!isShared && isCollection && ( -

Collection

+ {/* The lead block — title, cover image and metadata — is one large + click target via an overlay link, so tapping anywhere in it opens + the article. The source header above is deliberately excluded; the + interactive children (cover image, read button, shared-via link) + sit above the overlay with `relative z-1` and keep their own + behavior. The overlay is pointer-only (aria-hidden + tabIndex -1) + since the read button is the keyboard/AT path to the same place. */} +
+ {canReadArticle && ( + // eslint-disable-next-line jsx-a11y/anchor-has-content -- decorative pointer-only overlay; the read button below is the labeled keyboard/AT path to the same article + )} - {/* Title and image are top-aligned columns. The cover image opens a +
+ {sharedVia && ( +

+ Shared via + + + + {sharedVia.image && ( + + )} + {sharedVia.name} + + + + } + > + + +

+ )} + {isShared && !sharedVia && ( +

Shared post

+ )} + {!isShared && isCollection && ( +

Collection

+ )} + {/* Title and image are top-aligned columns. The cover image opens a lightbox rather than navigating away. The read button lives in the title column (right under the title) so it hugs the title regardless of the image height — a short title next to a tall image keeps the button close instead of dragging it down. */} -
-
-

- {/* The title is the largest, most forgiving target for opening - the source — it shares the read button's reader gate so - both routes behave identically. */} - {canReadArticle ? ( - - {title} - - ) : ( - title - )} -

- {renderReadButton('w-fit')} +
+
+

+ {title} +

+ {renderReadButton('relative z-1 w-fit')} +
+ {!isVideoType && article.image && ( + + )}
- {!isVideoType && article.image && ( - - )}
-
- 0 && ( - - From{' '} - - {article.domain} - - - ) - } - isVideoType={isVideoType} - readTime={article.readTime} - /> + 0 && ( + + From{' '} + + {article.domain} + + + ) + } + isVideoType={isVideoType} + readTime={article.readTime} + /> +
{isVideoType && (
Date: Sun, 28 Jun 2026 15:19:59 +0300 Subject: [PATCH 06/53] feat(post): add hover highlight to the lead click area Subtle surface-hover background behind the whole lead block so it reads as one clickable region. Applied to the wrapper (its background paints behind all content, keeping text crisp) with -m-2/p-2 for a rounded halo that doesn't shift layout, and gated on canReadArticle so native posts don't show a misleading hover. Co-Authored-By: Claude Opus 4.8 --- .../shared/src/components/post/focus/PostFocusCard.tsx | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/packages/shared/src/components/post/focus/PostFocusCard.tsx b/packages/shared/src/components/post/focus/PostFocusCard.tsx index c7e7c3cd6d..18c0ea60d6 100644 --- a/packages/shared/src/components/post/focus/PostFocusCard.tsx +++ b/packages/shared/src/components/post/focus/PostFocusCard.tsx @@ -384,7 +384,15 @@ export const PostFocusCard = ({ sit above the overlay with `relative z-1` and keep their own behavior. The overlay is pointer-only (aria-hidden + tabIndex -1) since the read button is the keyboard/AT path to the same place. */} -
+ {/* `-m-2 p-2` gives the hover highlight a rounded halo around the + lead area without shifting the layout; the wrapper's own + background paints behind all content so the text stays crisp. */} +
{canReadArticle && ( // eslint-disable-next-line jsx-a11y/anchor-has-content -- decorative pointer-only overlay; the read button below is the labeled keyboard/AT path to the same article Date: Sun, 28 Jun 2026 16:18:45 +0300 Subject: [PATCH 07/53] feat(post): underline title on lead-area hover instead of bg tint Swap the surface-hover background for a title underline on hover, using a named group on the lead wrapper. Cleaner "this links" affordance that doesn't recolour the whole region; gated on canReadArticle. Co-Authored-By: Claude Opus 4.8 --- .../src/components/post/focus/PostFocusCard.tsx | 14 +++++--------- 1 file changed, 5 insertions(+), 9 deletions(-) diff --git a/packages/shared/src/components/post/focus/PostFocusCard.tsx b/packages/shared/src/components/post/focus/PostFocusCard.tsx index 18c0ea60d6..e16e3f3fdb 100644 --- a/packages/shared/src/components/post/focus/PostFocusCard.tsx +++ b/packages/shared/src/components/post/focus/PostFocusCard.tsx @@ -384,15 +384,10 @@ export const PostFocusCard = ({ sit above the overlay with `relative z-1` and keep their own behavior. The overlay is pointer-only (aria-hidden + tabIndex -1) since the read button is the keyboard/AT path to the same place. */} - {/* `-m-2 p-2` gives the hover highlight a rounded halo around the - lead area without shifting the layout; the wrapper's own - background paints behind all content so the text stays crisp. */} -
+ {/* The whole lead area is one click target; hovering anywhere in it + underlines the title (the classic link affordance) so the region + clearly reads as clickable without recolouring its background. */} +
{canReadArticle && ( // eslint-disable-next-line jsx-a11y/anchor-has-content -- decorative pointer-only overlay; the read button below is the labeled keyboard/AT path to the same article From 737bc2e5814d0601e16b13f6b4e5e61c29672062 Mon Sep 17 00:00:00 2001 From: Tsahi Matsliah Date: Sun, 28 Jun 2026 16:19:45 +0300 Subject: [PATCH 08/53] style(post): enlarge cover thumbnail ~20% w-24 -> w-28 (mobile), w-40 -> w-48 (tablet). Co-Authored-By: Claude Opus 4.8 --- packages/shared/src/components/post/focus/PostFocusCard.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/shared/src/components/post/focus/PostFocusCard.tsx b/packages/shared/src/components/post/focus/PostFocusCard.tsx index e16e3f3fdb..115b405d56 100644 --- a/packages/shared/src/components/post/focus/PostFocusCard.tsx +++ b/packages/shared/src/components/post/focus/PostFocusCard.tsx @@ -468,7 +468,7 @@ export const PostFocusCard = ({
- {/* The lead block — title, cover image and metadata — is one large - click target via an overlay link, so tapping anywhere in it opens - the article. The source header above is deliberately excluded; the - interactive children (cover image, read button, shared-via link) - sit above the overlay with `relative z-1` and keep their own - behavior. The overlay is pointer-only (aria-hidden + tabIndex -1) - since the read button is the keyboard/AT path to the same place. */} - {/* The whole lead area is one click target; hovering anywhere in it - turns the title the link colour so the region clearly reads as - clickable without recolouring its background. */} -
- {canReadArticle && ( - // eslint-disable-next-line jsx-a11y/anchor-has-content -- decorative pointer-only overlay; the read button below is the labeled keyboard/AT path to the same article - - )} + {/* Only the title and the read button link to the post. The metadata + strip below (date, read time, source domain) is not part of that + click target, so hovering it neither shows a link cursor nor + colours the title — the source domain is its own separate link. */} +
{!isVideoType && article.image && ( - )} + {canReadArticle ? ( + + {title} + + ) : ( + title + )} + + {/* Date, read time and source sit directly under the title; + the read button moved to the bar below the summary. */} + 0 && ( + + From{' '} + + {article.domain} + + + ) + } + isVideoType={isVideoType} + readTime={article.readTime} + />
+ {!isVideoType && article.image && ( + + )}
- - 0 && ( - - From{' '} - {/* The source domain is its own link — blue + underline on - hover. */} - - {article.domain} - - - ) - } - isVideoType={isVideoType} - readTime={article.readTime} - />
{isVideoType && ( @@ -563,35 +555,28 @@ export const PostFocusCard = ({ )) )} - {/* Flat source link after the summary, styled like the "From - {domain}" metadata line (tertiary, typo-callout); only the domain - is the blue, underlined link. Shares the read button's reader - gate. */} - {canReadArticle && - (!isVideoType && article.domain ? ( -

- Read the full article on{' '} - - {article.domain} - -

- ) : ( - - {getReadPostButtonText(post)} - - ))} + {/* Full-width flat bar after the summary: the source label on the + left, the read button (moved out of the title column) on the + right. */} + {canReadArticle && ( +
+ {article.source?.image && ( + + )} + + {!isVideoType && article.domain + ? `Read the full article on ${article.domain}` + : 'Read the full article'} + + {renderReadButton('shrink-0')} +
+ )} From 3e717b5c4a0d86a2b33f66a9b1abef2c0732b40a Mon Sep 17 00:00:00 2001 From: Tsahi Matsliah Date: Mon, 29 Jun 2026 10:40:51 +0300 Subject: [PATCH 21/53] feat(post): flatten read CTA; add Storybook variants - Read CTA after the summary is now flat (no box/border/fill), drops the source image (already shown up top), and the read button hugs the text (w-fit, gap-3) instead of being pushed to a far edge. Button size -> Small for inline balance. - Add a Storybook design-review page (Components/Post/Read CTA variants) comparing six flat treatments (solid / subtle / outline / emphasized domain / icon-only / inline link) so we can pick a direction. Co-Authored-By: Claude Opus 4.8 --- .../components/post/focus/PostFocusCard.tsx | 20 +- .../components/PostReadCta.stories.tsx | 249 ++++++++++++++++++ 2 files changed, 254 insertions(+), 15 deletions(-) create mode 100644 packages/storybook/stories/components/PostReadCta.stories.tsx diff --git a/packages/shared/src/components/post/focus/PostFocusCard.tsx b/packages/shared/src/components/post/focus/PostFocusCard.tsx index 9e4ca960c6..abb323de5c 100644 --- a/packages/shared/src/components/post/focus/PostFocusCard.tsx +++ b/packages/shared/src/components/post/focus/PostFocusCard.tsx @@ -315,7 +315,7 @@ export const PostFocusCard = ({ } onClick={handleReadClick} variant={ButtonVariant.Primary} - size={ButtonSize.Medium} + size={ButtonSize.Small} className={className} > {getReadPostButtonText(post)} @@ -555,21 +555,11 @@ export const PostFocusCard = ({ )) )} - {/* Full-width flat bar after the summary: the source label on the - left, the read button (moved out of the title column) on the - right. */} + {/* Flat read CTA after the summary — descriptive text with the read + button hugging it on the right, no surrounding box. */} {canReadArticle && ( -
- {article.source?.image && ( - - )} - +
+ {!isVideoType && article.domain ? `Read the full article on ${article.domain}` : 'Read the full article'} diff --git a/packages/storybook/stories/components/PostReadCta.stories.tsx b/packages/storybook/stories/components/PostReadCta.stories.tsx new file mode 100644 index 0000000000..e308712a12 --- /dev/null +++ b/packages/storybook/stories/components/PostReadCta.stories.tsx @@ -0,0 +1,249 @@ +import type { Meta, StoryObj } from '@storybook/react-vite'; +import type { ReactElement, ReactNode } from 'react'; +import React from 'react'; +import { + Button, + ButtonVariant, + ButtonSize, + ButtonIconPosition, +} from '@dailydotdev/shared/src/components/buttons/Button'; +import { + Typography, + TypographyType, + TypographyColor, + TypographyTag, +} from '@dailydotdev/shared/src/components/typography/Typography'; +import { OpenLinkIcon } from '@dailydotdev/shared/src/components/icons'; +import { IconSize } from '@dailydotdev/shared/src/components/Icon'; + +/** + * Design-review playground (not shipping UI). Explores the flat "read the full + * article" call-to-action that sits right after the TL;DR in `PostFocusCard`. + * + * Constraints from the brief: flat (no box / border / fill), no source image + * (the avatar already shows at the top), and the read button hugging the + * descriptive text — close to it, not pushed to a far edge. Each variant is + * shown exactly where it lives: directly under the last line of the summary. + */ + +const DOMAIN = 'pragmaticengineer.com'; + +const ReadText = ({ + emphasizeDomain = false, +}: { + emphasizeDomain?: boolean; +}): ReactElement => ( + + Read the full article on{' '} + {emphasizeDomain ? ( + + {DOMAIN} + + ) : ( + DOMAIN + )} + +); + +type Variant = { + key: string; + name: string; + note: string; + node: ReactElement; +}; + +const variants: Variant[] = [ + { + key: 'A', + name: 'Solid primary', + note: 'Highest emphasis. The button clearly is the action; reads as the obvious exit.', + node: ( +
+ + +
+ ), + }, + { + key: 'B', + name: 'Subtle button', + note: 'Quieter — blends into the reading flow while still being a real button.', + node: ( +
+ + +
+ ), + }, + { + key: 'C', + name: 'Secondary outline', + note: 'Outlined button — medium emphasis, crisp edge without a heavy fill.', + node: ( +
+ + +
+ ), + }, + { + key: 'D', + name: 'Emphasized domain + solid', + note: 'Domain bolded in primary so the source pops next to the muted lead-in.', + node: ( +
+ + +
+ ), + }, + { + key: 'E', + name: 'Icon-only button', + note: 'Tightest pairing — text plus a compact icon button. Most "blended".', + node: ( +
+ +
+ ), + }, + { + key: 'F', + name: 'Inline link (no button)', + note: 'Lightest touch — one clickable line, domain in the link colour, trailing arrow. No button at all.', + node: ( + + Read the full article on{' '} + {DOMAIN} + + + ), + }, +]; + +const Row = ({ variant }: { variant: Variant }): ReactElement => ( +
+
+ + {variant.key} + + + {variant.name} + +
+ + {variant.note} + + {/* Shown in context: the last line of the summary, then the CTA below it. */} +
+ + …and here's the timeline, the root cause, and the three guardrails + the team added so a single index can't take down checkout again. + + {variant.node} +
+
+); + +const meta: Meta = { + title: 'Components/Post/Read CTA variants', + parameters: { + layout: 'fullscreen', + controls: { disable: true }, + }, +}; + +export default meta; + +type Story = StoryObj; + +export const AllVariants: Story = { + name: 'All variants', + render: () => ( +
+ + Flat read-the-article CTA + + + Flat, no box, no source image — the read button sits right beside the + text. Pick a direction; the chosen one ships in PostFocusCard after the + summary. + +
+ {variants.map((variant) => ( + + ))} +
+
+ ), +}; From cf98859d2a20466486c889a084124513c63f7531 Mon Sep 17 00:00:00 2001 From: Tsahi Matsliah Date: Mon, 29 Jun 2026 21:44:33 +0300 Subject: [PATCH 22/53] chore(storybook): layout-led read CTA variants Replace the button-style variants with bolder layout directions: big button left, big button + source meta, full-width block, text + round arrow, icon tile + stacked text, and centered with caption. Co-Authored-By: Claude Opus 4.8 --- .../components/PostReadCta.stories.tsx | 197 +++++++++--------- 1 file changed, 94 insertions(+), 103 deletions(-) diff --git a/packages/storybook/stories/components/PostReadCta.stories.tsx b/packages/storybook/stories/components/PostReadCta.stories.tsx index e308712a12..2760fbb1e5 100644 --- a/packages/storybook/stories/components/PostReadCta.stories.tsx +++ b/packages/storybook/stories/components/PostReadCta.stories.tsx @@ -13,41 +13,23 @@ import { TypographyColor, TypographyTag, } from '@dailydotdev/shared/src/components/typography/Typography'; -import { OpenLinkIcon } from '@dailydotdev/shared/src/components/icons'; +import { OpenLinkIcon, ArrowIcon } from '@dailydotdev/shared/src/components/icons'; import { IconSize } from '@dailydotdev/shared/src/components/Icon'; /** - * Design-review playground (not shipping UI). Explores the flat "read the full - * article" call-to-action that sits right after the TL;DR in `PostFocusCard`. - * - * Constraints from the brief: flat (no box / border / fill), no source image - * (the avatar already shows at the top), and the read button hugging the - * descriptive text — close to it, not pushed to a far edge. Each variant is - * shown exactly where it lives: directly under the last line of the summary. + * Design-review playground (not shipping UI). Bolder, layout-led directions for + * the "read the full article" CTA in `PostFocusCard` — these change the + * composition and placement of the action, not just the button's colour. Each + * is shown where it lives: right after the last line of the TL;DR. */ const DOMAIN = 'pragmaticengineer.com'; -const ReadText = ({ - emphasizeDomain = false, -}: { - emphasizeDomain?: boolean; -}): ReactElement => ( - +const Lead = (): ReactElement => ( + Read the full article on{' '} - {emphasizeDomain ? ( - - {DOMAIN} - - ) : ( - DOMAIN - )} - + {DOMAIN} +
); type Variant = { @@ -60,117 +42,126 @@ type Variant = { const variants: Variant[] = [ { key: 'A', - name: 'Solid primary', - note: 'Highest emphasis. The button clearly is the action; reads as the obvious exit.', + name: 'Big button, left', + note: 'One large, confident button leading the row — unmissable, reads as the primary action.', node: ( -
- - -
+ ), }, { key: 'B', - name: 'Subtle button', - note: 'Quieter — blends into the reading flow while still being a real button.', + name: 'Big button + source meta', + note: 'Large button on the left; the source and read time sit quietly to its right.', node: ( -
- +
+ + + {DOMAIN} + + 6 min read +
), }, { key: 'C', - name: 'Secondary outline', - note: 'Outlined button — medium emphasis, crisp edge without a heavy fill.', + name: 'Full-width block', + note: 'The whole strip is the button: label hugging the left, arrow pinned right. Maximum target.', node: ( -
- - -
+ ), }, { key: 'D', - name: 'Emphasized domain + solid', - note: 'Domain bolded in primary so the source pops next to the muted lead-in.', + name: 'Text left, round arrow right', + note: 'Reads like a row you advance through — copy on the left, a big circular “go” on the far right.', node: ( -
- +
+ + size={ButtonSize.Large} + icon={} + className="!rounded-full" + />
), }, { key: 'E', - name: 'Icon-only button', - note: 'Tightest pairing — text plus a compact icon button. Most "blended".', + name: 'Icon tile + stacked text', + note: 'App-row feel — a large open-link tile leads, with the action and source stacked beside it. Whole row is the link.', + node: ( + + + + + + + Read the full article + + + {DOMAIN} · 6 min read + + + + ), + }, + { + key: 'F', + name: 'Centered, with caption', + note: 'A confident centered button with a one-line caption — feels like an intentional “end of summary” moment.', node: ( -
- +
+ + Opens the original article +
), }, - { - key: 'F', - name: 'Inline link (no button)', - note: 'Lightest touch — one clickable line, domain in the link colour, trailing arrow. No button at all.', - node: ( - - Read the full article on{' '} - {DOMAIN} - - - ), - }, ]; const Row = ({ variant }: { variant: Variant }): ReactElement => ( @@ -191,12 +182,12 @@ const Row = ({ variant }: { variant: Variant }): ReactElement => ( {variant.note} - {/* Shown in context: the last line of the summary, then the CTA below it. */} -
+ {/* In context: the last line of the summary, then the CTA below it. */} +
…and here's the timeline, the root cause, and the three guardrails the team added so a single index can't take down checkout again. @@ -228,16 +219,16 @@ export const AllVariants: Story = { color={TypographyColor.Primary} bold > - Flat read-the-article CTA + Read-the-article CTA — layout directions - Flat, no box, no source image — the read button sits right beside the - text. Pick a direction; the chosen one ships in PostFocusCard after the - summary. + Different compositions and placements for the action, not just button + styles. Pick a direction; the chosen one ships in PostFocusCard after + the summary.
{variants.map((variant) => ( From b5b163f882a1eb9ee95bd807845cfd259050a8cc Mon Sep 17 00:00:00 2001 From: Tsahi Matsliah Date: Mon, 29 Jun 2026 21:47:35 +0300 Subject: [PATCH 23/53] chore(storybook): refine read CTA directions A, B, E Group the three favoured directions with sub-variants: - A: large vs xlarge, generic vs source-named label - B: stacked meta / divider+single-line / lead-in copy - E: subtle tile / outline tile / list-row with trailing arrow + hover Co-Authored-By: Claude Opus 4.8 --- .../components/PostReadCta.stories.tsx | 381 ++++++++++-------- 1 file changed, 210 insertions(+), 171 deletions(-) diff --git a/packages/storybook/stories/components/PostReadCta.stories.tsx b/packages/storybook/stories/components/PostReadCta.stories.tsx index 2760fbb1e5..b0f0c2b0f2 100644 --- a/packages/storybook/stories/components/PostReadCta.stories.tsx +++ b/packages/storybook/stories/components/PostReadCta.stories.tsx @@ -1,5 +1,5 @@ import type { Meta, StoryObj } from '@storybook/react-vite'; -import type { ReactElement, ReactNode } from 'react'; +import type { ReactElement } from 'react'; import React from 'react'; import { Button, @@ -13,188 +13,228 @@ import { TypographyColor, TypographyTag, } from '@dailydotdev/shared/src/components/typography/Typography'; -import { OpenLinkIcon, ArrowIcon } from '@dailydotdev/shared/src/components/icons'; +import { + OpenLinkIcon, + ArrowIcon, +} from '@dailydotdev/shared/src/components/icons'; import { IconSize } from '@dailydotdev/shared/src/components/Icon'; /** - * Design-review playground (not shipping UI). Bolder, layout-led directions for - * the "read the full article" CTA in `PostFocusCard` — these change the - * composition and placement of the action, not just the button's colour. Each - * is shown where it lives: right after the last line of the TL;DR. + * Design-review playground (not shipping UI). Deeper exploration of the three + * favoured directions for the "read the full article" CTA in `PostFocusCard`: + * A — big button, left + * B — big button + source meta + * E — icon tile + stacked text + * Each direction gets a few refinements so we can converge on one. Shown right + * after the last line of the TL;DR. */ const DOMAIN = 'pragmaticengineer.com'; -const Lead = (): ReactElement => ( - - Read the full article on{' '} - {DOMAIN} - -); +type Variant = { key: string; name: string; node: ReactElement }; +type Group = { letter: string; title: string; blurb: string; variants: Variant[] }; -type Variant = { - key: string; - name: string; - note: string; - node: ReactElement; -}; +const bigButton = ( + label: string, + size: ButtonSize = ButtonSize.Large, + className = 'w-fit', +): ReactElement => ( + +); -const variants: Variant[] = [ - { - key: 'A', - name: 'Big button, left', - note: 'One large, confident button leading the row — unmissable, reads as the primary action.', - node: ( - - ), - }, +const groups: Group[] = [ { - key: 'B', - name: 'Big button + source meta', - note: 'Large button on the left; the source and read time sit quietly to its right.', - node: ( -
- - - - {DOMAIN} - - 6 min read - -
- ), + letter: 'A', + title: 'Big button, left', + blurb: 'One confident button leading the row. Exploring label and size.', + variants: [ + { key: 'A1', name: 'Large · generic label', node: bigButton('Read the full article') }, + { key: 'A2', name: 'XLarge · punchy', node: bigButton('Read article', ButtonSize.XLarge) }, + { key: 'A3', name: 'Large · names the source', node: bigButton(`Read the full article on ${DOMAIN}`) }, + ], }, { - key: 'C', - name: 'Full-width block', - note: 'The whole strip is the button: label hugging the left, arrow pinned right. Maximum target.', - node: ( - - ), + letter: 'B', + title: 'Big button + source meta', + blurb: 'Large button leading; source + read time supporting it on the right.', + variants: [ + { + key: 'B1', + name: 'Stacked meta', + node: ( +
+ {bigButton('Read article')} + + + {DOMAIN} + + + 6 min read + + +
+ ), + }, + { + key: 'B2', + name: 'Divider + single line', + node: ( +
+ {bigButton('Read article')} + + + {DOMAIN} · 6 + min read + +
+ ), + }, + { + key: 'B3', + name: 'Lead-in copy + button', + node: ( +
+ + Continue on{' '} + {DOMAIN} + + {bigButton('Read article')} +
+ ), + }, + ], }, { - key: 'D', - name: 'Text left, round arrow right', - note: 'Reads like a row you advance through — copy on the left, a big circular “go” on the far right.', - node: ( -
- -
- ), - }, - { - key: 'E', - name: 'Icon tile + stacked text', - note: 'App-row feel — a large open-link tile leads, with the action and source stacked beside it. Whole row is the link.', - node: ( - - - - - - - Read the full article - - - {DOMAIN} · 6 min read - - - - ), - }, - { - key: 'F', - name: 'Centered, with caption', - note: 'A confident centered button with a one-line caption — feels like an intentional “end of summary” moment.', - node: ( -
- - - Opens the original article - -
- ), + letter: 'E', + title: 'Icon tile + stacked text', + blurb: 'App-row feel — a tile leads, action + source stacked beside it.', + variants: [ + { + key: 'E1', + name: 'Subtle tile', + node: ( + + + + + + + Read the full article + + + {DOMAIN} · 6 min read + + + + ), + }, + { + key: 'E2', + name: 'Outline tile', + node: ( + + + + + + + Read the full article + + + {DOMAIN} · 6 min read + + + + ), + }, + { + key: 'E3', + name: 'List row + trailing arrow + hover', + node: ( + + + + + + + Read the full article + + + {DOMAIN} · 6 min read + + + + + ), + }, + ], }, ]; -const Row = ({ variant }: { variant: Variant }): ReactElement => ( -
+const VariantRow = ({ variant }: { variant: Variant }): ReactElement => ( +
{variant.key} + + {variant.name} + +
+
+ + …and here's the timeline, the root cause, and the three guardrails + the team added so a single index can't take down checkout again. + + {variant.node} +
+
+); + +const GroupBlock = ({ group }: { group: Group }): ReactElement => ( +
+
- {variant.name} + {group.letter} · {group.title}
- {variant.note} + {group.blurb} - {/* In context: the last line of the summary, then the CTA below it. */} -
- - …and here's the timeline, the root cause, and the three guardrails - the team added so a single index can't take down checkout again. - - {variant.node} +
+ {group.variants.map((variant) => ( + + ))}
-
+
); const meta: Meta = { @@ -212,29 +252,28 @@ type Story = StoryObj; export const AllVariants: Story = { name: 'All variants', render: () => ( -
- - Read-the-article CTA — layout directions - - - Different compositions and placements for the action, not just button - styles. Pick a direction; the chosen one ships in PostFocusCard after - the summary. - -
- {variants.map((variant) => ( - - ))} +
+
+ + Read-the-article CTA — A · B · E, refined + + + Deeper takes on the three favoured directions. Pick a row; the chosen + one ships in PostFocusCard after the summary. +
+ {groups.map((group) => ( + + ))}
), }; From 2a6ee50e94c255c207a174c71a97807f3e6db5c9 Mon Sep 17 00:00:00 2001 From: Tsahi Matsliah Date: Mon, 29 Jun 2026 21:50:39 +0300 Subject: [PATCH 24/53] chore(storybook): converge read CTA on primary-tile list row Combine E2 + E3: a list row led by a square primary-coloured open-link button, action + source stacked beside it, whole-row hover. Four refinements (tile size, single vs two-line, trailing arrow, contained vs flat). Co-Authored-By: Claude Opus 4.8 --- .../components/PostReadCta.stories.tsx | 311 +++++++----------- 1 file changed, 114 insertions(+), 197 deletions(-) diff --git a/packages/storybook/stories/components/PostReadCta.stories.tsx b/packages/storybook/stories/components/PostReadCta.stories.tsx index b0f0c2b0f2..7affe4e131 100644 --- a/packages/storybook/stories/components/PostReadCta.stories.tsx +++ b/packages/storybook/stories/components/PostReadCta.stories.tsx @@ -5,7 +5,6 @@ import { Button, ButtonVariant, ButtonSize, - ButtonIconPosition, } from '@dailydotdev/shared/src/components/buttons/Button'; import { Typography, @@ -20,170 +19,114 @@ import { import { IconSize } from '@dailydotdev/shared/src/components/Icon'; /** - * Design-review playground (not shipping UI). Deeper exploration of the three - * favoured directions for the "read the full article" CTA in `PostFocusCard`: - * A — big button, left - * B — big button + source meta - * E — icon tile + stacked text - * Each direction gets a few refinements so we can converge on one. Shown right - * after the last line of the TL;DR. + * Design-review playground (not shipping UI). The converged direction for the + * "read the full article" CTA in `PostFocusCard`: a list row (E3) led by a + * square, primary-coloured open-link button (E2), action + source stacked + * beside it, with a whole-row hover. A few refinements to pick the final form. + * Shown right after the last line of the TL;DR. */ const DOMAIN = 'pragmaticengineer.com'; -type Variant = { key: string; name: string; node: ReactElement }; -type Group = { letter: string; title: string; blurb: string; variants: Variant[] }; - -const bigButton = ( - label: string, - size: ButtonSize = ButtonSize.Large, - className = 'w-fit', -): ReactElement => ( +const PrimaryTile = ({ + size = ButtonSize.Large, +}: { + size?: ButtonSize; +}): ReactElement => ( + className="!rounded-16 shrink-0" + /> +); + +const TrailingArrow = (): ReactElement => ( + ); -const groups: Group[] = [ +type Variant = { key: string; name: string; node: ReactElement }; + +const rowClass = + '-mx-2 flex w-full items-center gap-4 rounded-16 px-2 py-2 transition-colors hover:bg-surface-float'; + +const variants: Variant[] = [ { - letter: 'A', - title: 'Big button, left', - blurb: 'One confident button leading the row. Exploring label and size.', - variants: [ - { key: 'A1', name: 'Large · generic label', node: bigButton('Read the full article') }, - { key: 'A2', name: 'XLarge · punchy', node: bigButton('Read article', ButtonSize.XLarge) }, - { key: 'A3', name: 'Large · names the source', node: bigButton(`Read the full article on ${DOMAIN}`) }, - ], + key: 'T1', + name: 'Large tile · two-line · trailing arrow', + node: ( + + + + + Read the full article + + + {DOMAIN} · 6 min read + + + + + ), }, { - letter: 'B', - title: 'Big button + source meta', - blurb: 'Large button leading; source + read time supporting it on the right.', - variants: [ - { - key: 'B1', - name: 'Stacked meta', - node: ( -
- {bigButton('Read article')} - - - {DOMAIN} - - - 6 min read - - -
- ), - }, - { - key: 'B2', - name: 'Divider + single line', - node: ( -
- {bigButton('Read article')} - - - {DOMAIN} · 6 - min read - -
- ), - }, - { - key: 'B3', - name: 'Lead-in copy + button', - node: ( -
- - Continue on{' '} - {DOMAIN} - - {bigButton('Read article')} -
- ), - }, - ], + key: 'T2', + name: 'XLarge tile · two-line · no arrow', + node: ( + + + + + Read the full article + + + {DOMAIN} · 6 min read + + + + ), }, { - letter: 'E', - title: 'Icon tile + stacked text', - blurb: 'App-row feel — a tile leads, action + source stacked beside it.', - variants: [ - { - key: 'E1', - name: 'Subtle tile', - node: ( - - - - - - - Read the full article - - - {DOMAIN} · 6 min read - - - - ), - }, - { - key: 'E2', - name: 'Outline tile', - node: ( - - - - - - - Read the full article - - - {DOMAIN} · 6 min read - - - - ), - }, - { - key: 'E3', - name: 'List row + trailing arrow + hover', - node: ( - - - - - - - Read the full article - - - {DOMAIN} · 6 min read - - - - - ), - }, - ], + key: 'T3', + name: 'Large tile · single line · trailing arrow', + node: ( + + + + Read the full article on{' '} + {DOMAIN} + + + + ), + }, + { + key: 'T4', + name: 'Contained row · hairline border', + node: ( + + + + + Read the full article + + + {DOMAIN} · 6 min read + + + + + ), }, ]; @@ -200,7 +143,7 @@ const VariantRow = ({ variant }: { variant: Variant }): ReactElement => ( {variant.name}
-
+
…and here's the timeline, the root cause, and the three guardrails the team added so a single index can't take down checkout again. @@ -210,33 +153,6 @@ const VariantRow = ({ variant }: { variant: Variant }): ReactElement => (
); -const GroupBlock = ({ group }: { group: Group }): ReactElement => ( -
-
- - {group.letter} · {group.title} - -
- - {group.blurb} - -
- {group.variants.map((variant) => ( - - ))} -
-
-); - const meta: Meta = { title: 'Components/Post/Read CTA variants', parameters: { @@ -252,28 +168,29 @@ type Story = StoryObj; export const AllVariants: Story = { name: 'All variants', render: () => ( -
-
- - Read-the-article CTA — A · B · E, refined - - - Deeper takes on the three favoured directions. Pick a row; the chosen - one ships in PostFocusCard after the summary. - +
+ + Read-the-article CTA — primary tile row + + + A list row led by a square, primary-coloured open-link button, action + and source stacked beside it, with a whole-row hover. Pick the final + form; it ships in PostFocusCard after the summary. + +
+ {variants.map((variant) => ( + + ))}
- {groups.map((group) => ( - - ))}
), }; From e487e9cc6372bbfcad0eee3ac101d5a102b5322b Mon Sep 17 00:00:00 2001 From: Tsahi Matsliah Date: Mon, 29 Jun 2026 21:58:24 +0300 Subject: [PATCH 25/53] chore(storybook): primary tile row with interface-feel motion T1 without the trailing arrow, exploring jakub.kr-style micro-interactions: icon lift-off, springy tile pop + shadow, content glide, and a magnetic tile that follows the cursor. Snappy/spring easings. Co-Authored-By: Claude Opus 4.8 --- .../components/PostReadCta.stories.tsx | 217 +++++++++++------- 1 file changed, 136 insertions(+), 81 deletions(-) diff --git a/packages/storybook/stories/components/PostReadCta.stories.tsx b/packages/storybook/stories/components/PostReadCta.stories.tsx index 7affe4e131..e7d6a32a43 100644 --- a/packages/storybook/stories/components/PostReadCta.stories.tsx +++ b/packages/storybook/stories/components/PostReadCta.stories.tsx @@ -1,6 +1,7 @@ import type { Meta, StoryObj } from '@storybook/react-vite'; import type { ReactElement } from 'react'; -import React from 'react'; +import React, { useRef } from 'react'; +import classNames from 'classnames'; import { Button, ButtonVariant, @@ -12,122 +13,176 @@ import { TypographyColor, TypographyTag, } from '@dailydotdev/shared/src/components/typography/Typography'; -import { - OpenLinkIcon, - ArrowIcon, -} from '@dailydotdev/shared/src/components/icons'; -import { IconSize } from '@dailydotdev/shared/src/components/Icon'; +import { OpenLinkIcon } from '@dailydotdev/shared/src/components/icons'; /** - * Design-review playground (not shipping UI). The converged direction for the - * "read the full article" CTA in `PostFocusCard`: a list row (E3) led by a - * square, primary-coloured open-link button (E2), action + source stacked - * beside it, with a whole-row hover. A few refinements to pick the final form. - * Shown right after the last line of the TL;DR. + * Design-review playground (not shipping UI). The chosen direction — a list row + * led by a square, primary open-link button (T1, no trailing arrow) — explored + * with interface-feel motion in the spirit of jakub.kr: fast, springy, + * purposeful micro-interactions on hover/press. Shown right after the TL;DR. */ const DOMAIN = 'pragmaticengineer.com'; -const PrimaryTile = ({ - size = ButtonSize.Large, +// Snappy "settle" and a slight overshoot for the tactile bits. +const EASE_SNAP = 'ease-[cubic-bezier(0.2,0.7,0.2,1)]'; +const EASE_SPRING = 'ease-[cubic-bezier(0.34,1.56,0.64,1)]'; + +const Tile = ({ + iconClassName, + className, }: { - size?: ButtonSize; + iconClassName?: string; + className?: string; }): ReactElement => ( - ) : null; - return (
+ + + Read the full article + + {sourceMeta && ( + + {sourceMeta} + + )} + + )} diff --git a/packages/storybook/stories/components/PostReadCta.stories.tsx b/packages/storybook/stories/components/PostReadCta.stories.tsx index e7d6a32a43..6d3678661a 100644 --- a/packages/storybook/stories/components/PostReadCta.stories.tsx +++ b/packages/storybook/stories/components/PostReadCta.stories.tsx @@ -14,20 +14,30 @@ import { TypographyTag, } from '@dailydotdev/shared/src/components/typography/Typography'; import { OpenLinkIcon } from '@dailydotdev/shared/src/components/icons'; +import { IconSize } from '@dailydotdev/shared/src/components/Icon'; /** * Design-review playground (not shipping UI). The chosen direction — a list row - * led by a square, primary open-link button (T1, no trailing arrow) — explored - * with interface-feel motion in the spirit of jakub.kr: fast, springy, - * purposeful micro-interactions on hover/press. Shown right after the TL;DR. + * led by a square, primary open-link button (no trailing arrow) — in two + * stories: + * • "All variants" — interface-feel motion (jakub.kr spirit): hover/press + * micro-interactions. M1 (icon lift-off) + M2 (tile pop) ship in + * PostFocusCard. + * • "Prominent" — the same expression turned up: filled rows, a full primary + * block, an XL tile, and a bordered card. + * Shown right after the last line of the TL;DR. */ const DOMAIN = 'pragmaticengineer.com'; -// Snappy "settle" and a slight overshoot for the tactile bits. const EASE_SNAP = 'ease-[cubic-bezier(0.2,0.7,0.2,1)]'; const EASE_SPRING = 'ease-[cubic-bezier(0.34,1.56,0.64,1)]'; +const nudge = classNames( + 'transition-transform duration-200 group-hover:-translate-y-0.5 group-hover:translate-x-0.5 motion-reduce:transition-none', + EASE_SNAP, +); + const Tile = ({ iconClassName, className, @@ -46,6 +56,27 @@ const Tile = ({ /> ); +// Decorative primary tile (a span, not a button) for the prominent rows where +// the row itself is the link. +const SpanTile = ({ + sizeClass = 'size-12', + iconSize = IconSize.Large, +}: { + sizeClass?: string; + iconSize?: IconSize; +}): ReactElement => ( + + + +); + const Text = (): ReactElement => ( @@ -60,10 +91,6 @@ const Text = (): ReactElement => ( const baseRow = 'group -mx-2 flex w-full items-center gap-4 rounded-16 px-2 py-2 hover:bg-surface-float'; -/** - * Magnetic tile — the open-link button drifts a little toward the cursor as it - * crosses the row, then springs home on leave. The jakub.kr "alive" touch. - */ const MagneticRow = (): ReactElement => { const ref = useRef(null); const onMove = (e: React.MouseEvent): void => { @@ -104,7 +131,7 @@ const MagneticRow = (): ReactElement => { type Variant = { key: string; name: string; node: ReactElement }; -const variants: Variant[] = [ +const motionVariants: Variant[] = [ { key: 'M1', name: 'Icon lift-off — the arrow leaves on hover', @@ -113,16 +140,11 @@ const variants: Variant[] = [ href="#" className={classNames( baseRow, - 'transition-[background-color,transform] duration-200 active:scale-[0.99]', + 'w-fit transition-[background-color,transform] duration-200 active:scale-[0.99] motion-reduce:transition-none', EASE_SNAP, )} > - + ), @@ -135,13 +157,13 @@ const variants: Variant[] = [ href="#" className={classNames( baseRow, - 'transition-colors duration-200 active:scale-[0.99]', + 'w-fit transition-colors duration-200 active:scale-[0.99] motion-reduce:transition-none', EASE_SNAP, )} > @@ -150,31 +172,25 @@ const variants: Variant[] = [ ), }, { - key: 'M3', - name: 'Glide — content advances, icon leans out', + key: 'M1+M2', + name: 'Combined — lift-off + pop (ships in PostFocusCard)', node: ( - - - - + /> + ), }, @@ -185,6 +201,83 @@ const variants: Variant[] = [ }, ]; +const prominentVariants: Variant[] = [ + { + key: 'P1', + name: 'Filled row — always tinted, full width', + node: ( + + + + + ), + }, + { + key: 'P2', + name: 'Primary block — the whole row is the button', + node: ( + + + + Read the full article on {DOMAIN} + + + ), + }, + { + key: 'P3', + name: 'XL tile + bigger heading', + node: ( + + + + + Read the full article + + + {DOMAIN} · 6 min read + + + + ), + }, + { + key: 'P4', + name: 'Bordered card row', + node: ( + + + + + ), + }, +]; + const VariantRow = ({ variant }: { variant: Variant }): ReactElement => (
@@ -208,6 +301,39 @@ const VariantRow = ({ variant }: { variant: Variant }): ReactElement => (
); +const Page = ({ + title, + blurb, + list, +}: { + title: string; + blurb: string; + list: Variant[]; +}): ReactElement => ( +
+ + {title} + + + {blurb} + +
+ {list.map((variant) => ( + + ))} +
+
+); + const meta: Meta = { title: 'Components/Post/Read CTA variants', parameters: { @@ -223,29 +349,21 @@ type Story = StoryObj; export const AllVariants: Story = { name: 'All variants', render: () => ( -
- - Read-the-article CTA — primary tile row, with feel - - - T1 without the trailing arrow — the white open-link button already is - the signal. Each row adds a different interface-feel touch (hover over - them). Pick the motion; it ships in PostFocusCard after the summary. - -
- {variants.map((variant) => ( - - ))} -
-
+ + ), +}; + +export const Prominent: Story = { + name: 'Prominent', + render: () => ( + ), }; From 052fb0ba31e817ef7c72b57a46900d5861e13c79 Mon Sep 17 00:00:00 2001 From: Tsahi Matsliah Date: Tue, 30 Jun 2026 10:14:15 +0300 Subject: [PATCH 27/53] =?UTF-8?q?chore(storybook):=20add=20"M2=20variation?= =?UTF-8?q?s"=20=E2=80=94=20ten=20tile-pop=20takes,=20no=20source=20name?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Ten layouts on the M2 springy-tile-pop row with the source name dropped: single line, two-line + read time, XL tile, filled row, primary block, tile on the right, centered, full-width pinned-right, bordered card, compact pill. Co-Authored-By: Claude Opus 4.8 --- .../components/PostReadCta.stories.tsx | 281 ++++++++++++------ 1 file changed, 189 insertions(+), 92 deletions(-) diff --git a/packages/storybook/stories/components/PostReadCta.stories.tsx b/packages/storybook/stories/components/PostReadCta.stories.tsx index 6d3678661a..1ae2336b98 100644 --- a/packages/storybook/stories/components/PostReadCta.stories.tsx +++ b/packages/storybook/stories/components/PostReadCta.stories.tsx @@ -18,13 +18,10 @@ import { IconSize } from '@dailydotdev/shared/src/components/Icon'; /** * Design-review playground (not shipping UI). The chosen direction — a list row - * led by a square, primary open-link button (no trailing arrow) — in two - * stories: - * • "All variants" — interface-feel motion (jakub.kr spirit): hover/press - * micro-interactions. M1 (icon lift-off) + M2 (tile pop) ship in - * PostFocusCard. - * • "Prominent" — the same expression turned up: filled rows, a full primary - * block, an XL tile, and a bordered card. + * led by a square, primary open-link tile — across three stories: + * • All variants — interface-feel motion (jakub.kr spirit). M1 + M2 ship. + * • Prominent — the same expression turned up. + * • M2 variations — ten takes on the springy-tile-pop row, no source name. * Shown right after the last line of the TL;DR. */ @@ -38,26 +35,8 @@ const nudge = classNames( EASE_SNAP, ); -const Tile = ({ - iconClassName, - className, -}: { - iconClassName?: string; - className?: string; -}): ReactElement => ( -
@@ -351,7 +437,7 @@ export const AllVariants: Story = { render: () => ( ), @@ -362,8 +448,19 @@ export const Prominent: Story = { render: () => ( ), }; + +export const M2Variations: Story = { + name: 'M2 variations', + render: () => ( + + ), +}; From 1ef602cceb4e5df3b6f547731426838cd41d8777 Mon Sep 17 00:00:00 2001 From: Tsahi Matsliah Date: Tue, 30 Jun 2026 10:23:05 +0300 Subject: [PATCH 28/53] feat(post): read CTA as content-width primary block with subtitle MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Switch the shipped CTA to variant 05 refined: a primary block sized to its content (w-fit) with a two-line label — "Read the full article" over a muted source · read time subtitle. Hover lifts the block + nudges the icon. Storybook: refine M2 #05 to match and add #05b (read-time-only subtitle). Co-Authored-By: Claude Opus 4.8 --- .../components/post/focus/PostFocusCard.tsx | 29 +++++++----------- .../components/PostReadCta.stories.tsx | 30 ++++++++++++++++--- 2 files changed, 37 insertions(+), 22 deletions(-) diff --git a/packages/shared/src/components/post/focus/PostFocusCard.tsx b/packages/shared/src/components/post/focus/PostFocusCard.tsx index 5bebfa6836..b439f7a76d 100644 --- a/packages/shared/src/components/post/focus/PostFocusCard.tsx +++ b/packages/shared/src/components/post/focus/PostFocusCard.tsx @@ -527,10 +527,10 @@ export const PostFocusCard = ({ )) )} - {/* Read CTA after the summary — a list row led by a square, primary - open-link tile. The row itself is the link (the tile is decorative, - so we never nest interactive elements); on hover the tile pops and - the icon lifts off, honouring reduced-motion. */} + {/* Read CTA after the summary — a single primary block sized to its + content. The whole block is the link; on hover it lifts with a soft + shadow and the open-link icon nudges out, honouring reduced-motion. + A muted second line carries the source and read time. */} {canReadArticle && ( - - - + - + Read the full article {sourceMeta && ( - - {sourceMeta} - + {sourceMeta} )} diff --git a/packages/storybook/stories/components/PostReadCta.stories.tsx b/packages/storybook/stories/components/PostReadCta.stories.tsx index 1ae2336b98..99336375ac 100644 --- a/packages/storybook/stories/components/PostReadCta.stories.tsx +++ b/packages/storybook/stories/components/PostReadCta.stories.tsx @@ -289,17 +289,39 @@ const m2Variants: Variant[] = [ }, { key: '05', - name: 'Primary block — whole row', + name: 'Primary block · w-fit · source + read time (ships)', node: ( - Read the full article + + Read the full article + {DOMAIN} · 6 min read + + + ), + }, + { + key: '05b', + name: 'Primary block · w-fit · read time only', + node: ( + + + + Read the full article + 6 min read + ), }, From 069431a5076c0f3549e24d8215869329c1d08d2d Mon Sep 17 00:00:00 2001 From: Tsahi Matsliah Date: Tue, 30 Jun 2026 10:29:18 +0300 Subject: [PATCH 29/53] feat(post): soften read CTA label + add "05 explorations" MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Component: lighten the block label (font-bold -> font-medium), mute the subtitle a touch more, and add optical right padding (pl-5 pr-6) so the text isn't cramped opposite the icon. Storybook: add "05 explorations" — ten takes on the primary block (medium / secondary / tertiary label, icon left or right, optical padding). Co-Authored-By: Claude Opus 4.8 --- .../components/post/focus/PostFocusCard.tsx | 6 +- .../components/PostReadCta.stories.tsx | 99 +++++++++++++++++++ 2 files changed, 102 insertions(+), 3 deletions(-) diff --git a/packages/shared/src/components/post/focus/PostFocusCard.tsx b/packages/shared/src/components/post/focus/PostFocusCard.tsx index b439f7a76d..08b94d9023 100644 --- a/packages/shared/src/components/post/focus/PostFocusCard.tsx +++ b/packages/shared/src/components/post/focus/PostFocusCard.tsx @@ -542,18 +542,18 @@ export const PostFocusCard = ({ ? `Read the full article on ${article.domain}` : 'Read the full article' } - className="group flex w-fit items-center gap-3 rounded-16 bg-text-primary px-5 py-3 text-surface-invert transition-[transform,box-shadow] duration-200 ease-[cubic-bezier(0.34,1.56,0.64,1)] hover:-translate-y-0.5 hover:shadow-3 active:translate-y-0 active:scale-[0.99] motion-reduce:transition-none" + className="group flex w-fit items-center gap-3 rounded-16 bg-text-primary py-3 pl-5 pr-6 text-surface-invert transition-[transform,box-shadow] duration-200 ease-[cubic-bezier(0.34,1.56,0.64,1)] hover:-translate-y-0.5 hover:shadow-3 active:translate-y-0 active:scale-[0.99] motion-reduce:transition-none" > - + Read the full article {sourceMeta && ( - {sourceMeta} + {sourceMeta} )} diff --git a/packages/storybook/stories/components/PostReadCta.stories.tsx b/packages/storybook/stories/components/PostReadCta.stories.tsx index 99336375ac..de46c2f79d 100644 --- a/packages/storybook/stories/components/PostReadCta.stories.tsx +++ b/packages/storybook/stories/components/PostReadCta.stories.tsx @@ -389,6 +389,94 @@ const m2Variants: Variant[] = [ }, ]; +// Ten takes on the 05 primary block: softer label colours, icon on either +// side, and optical padding (a touch more room opposite the icon). +const blockBase = + 'group flex w-fit items-center rounded-16 bg-text-primary text-surface-invert transition-[transform,box-shadow] duration-200 hover:-translate-y-0.5 hover:shadow-3 active:translate-y-0 active:scale-[0.99] motion-reduce:transition-none'; + +const BlockIcon = (): ReactElement => ( + +); + +const Block = ({ + iconRight = false, + titleClass = 'font-medium', + subtitle, + pad = 'pl-5 pr-6 py-3', + gap = 'gap-3', +}: { + iconRight?: boolean; + titleClass?: string; + subtitle?: string; + pad?: string; + gap?: string; +}): ReactElement => ( + + {!iconRight && } + + + Read the full article + + {subtitle && {subtitle}} + + {iconRight && } + +); + +const SOURCE = `${DOMAIN} · 6 min read`; + +const block05Variants: Variant[] = [ + { key: '01', name: 'Icon left · medium weight', node: }, + { + key: '02', + name: 'Icon left · secondary (80%)', + node: , + }, + { + key: '03', + name: 'Icon left · tertiary (65%)', + node: , + }, + { + key: '04', + name: 'Icon left · roomy right gap', + node: , + }, + { key: '05', name: 'Icon left · single line', node: }, + { + key: '06', + name: 'Icon right · medium weight', + node: , + }, + { + key: '07', + name: 'Icon right · bold', + node: , + }, + { + key: '08', + name: 'Icon right · secondary (80%)', + node: ( + + ), + }, + { + key: '09', + name: 'Icon right · single line', + node: , + }, + { + key: '10', + name: 'Icon right · roomy · read time only', + node: , + }, +]; + const VariantRow = ({ variant }: { variant: Variant }): ReactElement => (
@@ -486,3 +574,14 @@ export const M2Variations: Story = { /> ), }; + +export const Block05Variations: Story = { + name: '05 explorations', + render: () => ( + + ), +}; From 9ffbf67919ff080449536855161e3bd0d0a4325b Mon Sep 17 00:00:00 2001 From: Tsahi Matsliah Date: Tue, 30 Jun 2026 10:34:03 +0300 Subject: [PATCH 30/53] fix(post): keep CTA title bold, mute the source line MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Revert the title to full-strength bold and keep the source · read time subtitle as the muted secondary line (per feedback — it was the subtitle, not the title, that should read quieter). Co-Authored-By: Claude Opus 4.8 --- packages/shared/src/components/post/focus/PostFocusCard.tsx | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/shared/src/components/post/focus/PostFocusCard.tsx b/packages/shared/src/components/post/focus/PostFocusCard.tsx index 08b94d9023..2b99058642 100644 --- a/packages/shared/src/components/post/focus/PostFocusCard.tsx +++ b/packages/shared/src/components/post/focus/PostFocusCard.tsx @@ -549,10 +549,12 @@ export const PostFocusCard = ({ className="shrink-0 transition-transform duration-200 ease-[cubic-bezier(0.2,0.7,0.2,1)] group-hover:-translate-y-0.5 group-hover:translate-x-0.5 motion-reduce:transition-none" /> - + Read the full article {sourceMeta && ( + // Source · read time is the muted secondary line; the title + // above stays full strength. {sourceMeta} )} From 3b10e6039084817140a1edb9d9d35b26934911fb Mon Sep 17 00:00:00 2001 From: Tsahi Matsliah Date: Tue, 30 Jun 2026 10:39:07 +0300 Subject: [PATCH 31/53] feat(post): single-line read CTA, icon on the right MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Drop the source · read time subtitle — the CTA is just "Read the full article" with the open-link icon moved to the right (optical padding flipped to more room on the left). Remove the now-unused sourceMeta. Co-Authored-By: Claude Opus 4.8 --- .../components/post/focus/PostFocusCard.tsx | 19 ++----------------- 1 file changed, 2 insertions(+), 17 deletions(-) diff --git a/packages/shared/src/components/post/focus/PostFocusCard.tsx b/packages/shared/src/components/post/focus/PostFocusCard.tsx index 2b99058642..05569cbf3d 100644 --- a/packages/shared/src/components/post/focus/PostFocusCard.tsx +++ b/packages/shared/src/components/post/focus/PostFocusCard.tsx @@ -249,12 +249,6 @@ export const PostFocusCard = ({ const [isVideoExpanded, setIsVideoExpanded] = useState(false); const readHref = getReadArticleHref(post); const canReadArticle = !!readHref && !isInternalReadType(post); - const sourceMeta = [ - !isVideoType && article.domain ? article.domain : undefined, - article.readTime ? `${article.readTime} min read` : undefined, - ] - .filter(Boolean) - .join(' · '); useEffect(() => { if (!isVideoType || isVideoExpanded) { @@ -542,22 +536,13 @@ export const PostFocusCard = ({ ? `Read the full article on ${article.domain}` : 'Read the full article' } - className="group flex w-fit items-center gap-3 rounded-16 bg-text-primary py-3 pl-5 pr-6 text-surface-invert transition-[transform,box-shadow] duration-200 ease-[cubic-bezier(0.34,1.56,0.64,1)] hover:-translate-y-0.5 hover:shadow-3 active:translate-y-0 active:scale-[0.99] motion-reduce:transition-none" + className="group flex w-fit items-center gap-3 rounded-16 bg-text-primary py-3 pl-6 pr-5 text-surface-invert transition-[transform,box-shadow] duration-200 ease-[cubic-bezier(0.34,1.56,0.64,1)] hover:-translate-y-0.5 hover:shadow-3 active:translate-y-0 active:scale-[0.99] motion-reduce:transition-none" > + Read the full article - - - Read the full article - - {sourceMeta && ( - // Source · read time is the muted secondary line; the title - // above stays full strength. - {sourceMeta} - )} - )} From 027bb0ab4a9967a241ec2b418bd9ee5bb338c9e3 Mon Sep 17 00:00:00 2001 From: Tsahi Matsliah Date: Tue, 30 Jun 2026 11:18:23 +0300 Subject: [PATCH 32/53] style(post): shrink read CTA one size down MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Smaller padding (py-2, pl-4 pr-3), typo-callout label, IconSize.Medium, rounded-12 — a more compact read CTA. Co-Authored-By: Claude Opus 4.8 --- packages/shared/src/components/post/focus/PostFocusCard.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/shared/src/components/post/focus/PostFocusCard.tsx b/packages/shared/src/components/post/focus/PostFocusCard.tsx index 05569cbf3d..4a3094b793 100644 --- a/packages/shared/src/components/post/focus/PostFocusCard.tsx +++ b/packages/shared/src/components/post/focus/PostFocusCard.tsx @@ -536,11 +536,11 @@ export const PostFocusCard = ({ ? `Read the full article on ${article.domain}` : 'Read the full article' } - className="group flex w-fit items-center gap-3 rounded-16 bg-text-primary py-3 pl-6 pr-5 text-surface-invert transition-[transform,box-shadow] duration-200 ease-[cubic-bezier(0.34,1.56,0.64,1)] hover:-translate-y-0.5 hover:shadow-3 active:translate-y-0 active:scale-[0.99] motion-reduce:transition-none" + className="group flex w-fit items-center gap-2 rounded-12 bg-text-primary py-2 pl-4 pr-3 text-surface-invert transition-[transform,box-shadow] duration-200 ease-[cubic-bezier(0.34,1.56,0.64,1)] hover:-translate-y-0.5 hover:shadow-3 active:translate-y-0 active:scale-[0.99] motion-reduce:transition-none" > - Read the full article + Read the full article From e124b6e3a53cdb4909b3ea9e8f0c371189bca85c Mon Sep 17 00:00:00 2001 From: Tsahi Matsliah Date: Tue, 30 Jun 2026 11:19:02 +0300 Subject: [PATCH 33/53] style(post): prettier wrap read CTA label Co-Authored-By: Claude Opus 4.8 --- packages/shared/src/components/post/focus/PostFocusCard.tsx | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/shared/src/components/post/focus/PostFocusCard.tsx b/packages/shared/src/components/post/focus/PostFocusCard.tsx index 4a3094b793..abbac9bd3c 100644 --- a/packages/shared/src/components/post/focus/PostFocusCard.tsx +++ b/packages/shared/src/components/post/focus/PostFocusCard.tsx @@ -538,7 +538,9 @@ export const PostFocusCard = ({ } className="group flex w-fit items-center gap-2 rounded-12 bg-text-primary py-2 pl-4 pr-3 text-surface-invert transition-[transform,box-shadow] duration-200 ease-[cubic-bezier(0.34,1.56,0.64,1)] hover:-translate-y-0.5 hover:shadow-3 active:translate-y-0 active:scale-[0.99] motion-reduce:transition-none" > - Read the full article + + Read the full article + Date: Tue, 30 Jun 2026 11:21:57 +0300 Subject: [PATCH 34/53] feat(post): read CTA above the TL;DR on mobile Define the read CTA once and place it twice behind breakpoint-gated wrappers: above the summary on mobile (tablet:hidden), after the summary on desktop (hidden tablet:block). Co-Authored-By: Claude Opus 4.8 --- .../components/post/focus/PostFocusCard.tsx | 54 ++++++++++--------- 1 file changed, 28 insertions(+), 26 deletions(-) diff --git a/packages/shared/src/components/post/focus/PostFocusCard.tsx b/packages/shared/src/components/post/focus/PostFocusCard.tsx index abbac9bd3c..e81ab4b826 100644 --- a/packages/shared/src/components/post/focus/PostFocusCard.tsx +++ b/packages/shared/src/components/post/focus/PostFocusCard.tsx @@ -288,6 +288,29 @@ export const PostFocusCard = ({ focusCommentRef.current(); }; + // The read CTA sits above the TL;DR on mobile and after it on desktop, so it + // is defined once and placed twice behind breakpoint-gated wrappers. + const readCta = canReadArticle ? ( + + Read the full article + + + ) : null; + return (
)} + {/* Mobile: the read CTA sits above the TL;DR. */} + {readCta &&
{readCta}
} + {article.contentHtml ? ( <> @@ -521,32 +547,8 @@ export const PostFocusCard = ({ )) )} - {/* Read CTA after the summary — a single primary block sized to its - content. The whole block is the link; on hover it lifts with a soft - shadow and the open-link icon nudges out, honouring reduced-motion. - A muted second line carries the source and read time. */} - {canReadArticle && ( - - - Read the full article - - - - )} + {/* Desktop: the read CTA sits after the summary. */} + {readCta &&
{readCta}
} From dbeb9a6cea472032126e5aa1a02fd50f3e8947e2 Mon Sep 17 00:00:00 2001 From: Tsahi Matsliah Date: Tue, 30 Jun 2026 11:58:35 +0300 Subject: [PATCH 35/53] fix(post): wrap source domain on mobile instead of truncating Let the under-title metadata wrap to a second line on mobile so the full domain stays visible (no ellipsis); keep the single-line truncated layout from tablet up. Co-Authored-By: Claude Opus 4.8 --- .../shared/src/components/post/focus/PostFocusCard.tsx | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/packages/shared/src/components/post/focus/PostFocusCard.tsx b/packages/shared/src/components/post/focus/PostFocusCard.tsx index e81ab4b826..ad8c30b206 100644 --- a/packages/shared/src/components/post/focus/PostFocusCard.tsx +++ b/packages/shared/src/components/post/focus/PostFocusCard.tsx @@ -30,7 +30,6 @@ import { cloudinaryPostImageCoverPlaceholder } from '../../../lib/image'; import { ButtonSize, ButtonVariant } from '../../buttons/Button'; import { PostUpvotesCommentsCount } from '../PostUpvotesCommentsCount'; import { PostTagList } from '../tags/PostTagList'; -import { TruncateText } from '../../utilities'; import { combinedClicks } from '../../../lib/click'; import { useFeature } from '../../GrowthBookProvider'; import { feature } from '../../../lib/featureManagement'; @@ -444,13 +443,15 @@ export const PostFocusCard = ({ {/* Date, read time and source sit directly under the title; the read button moved to the bar below the summary. */} 0 && ( - + From{' '} {article.domain} - + ) } isVideoType={isVideoType} From 5c48828af5c3dcc3cfcd2f397fbd19f460ba978c Mon Sep 17 00:00:00 2001 From: Tsahi Matsliah Date: Tue, 30 Jun 2026 13:10:12 +0300 Subject: [PATCH 36/53] fix(post): even out spacing around the metadata line MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Move date · read time · source out of the title column to a full-width line directly under the title row. Its gap to the TL;DR is now the column's gap-4, matching TL;DR -> button and button -> tags, instead of varying with the cover image height. Co-Authored-By: Claude Opus 4.8 --- .../components/post/focus/PostFocusCard.tsx | 55 ++++++++++--------- 1 file changed, 28 insertions(+), 27 deletions(-) diff --git a/packages/shared/src/components/post/focus/PostFocusCard.tsx b/packages/shared/src/components/post/focus/PostFocusCard.tsx index ad8c30b206..07e74b42be 100644 --- a/packages/shared/src/components/post/focus/PostFocusCard.tsx +++ b/packages/shared/src/components/post/focus/PostFocusCard.tsx @@ -440,33 +440,6 @@ export const PostFocusCard = ({ title )} - {/* Date, read time and source sit directly under the title; - the read button moved to the bar below the summary. */} - 0 && ( - - From{' '} - - {article.domain} - - - ) - } - isVideoType={isVideoType} - readTime={article.readTime} - />
{!isVideoType && article.image && (
+ {/* Date, read time and source on a full-width line under the title + row, so its gap to the TL;DR matches the rest of the column. */} + 0 && ( + + From{' '} + + {article.domain} + + + ) + } + isVideoType={isVideoType} + readTime={article.readTime} + /> + {isVideoType && (
Date: Wed, 1 Jul 2026 11:31:36 +0300 Subject: [PATCH 37/53] feat(post): float engagement bar at the bottom, not the top Per community feedback the sticky-top engagement bar felt off (top is for nav; no reading platform puts engagement there). Make FocusCardActionBar float as a centred pill pinned to the BOTTOM of the scroll area on tablet+ (w-fit mx-auto, sticky bottom-4), matching the reader and mobile bars. Drop the sticky-top machinery: the stuck-top close-X, isStuckTop detection, the top offset / rail-header logic, and the now-unused onClose prop, CloseButton and useLayoutVariant imports. Same component powers the post page and the post modal, so both are covered. Co-Authored-By: Claude Opus 4.8 --- .../post/focus/FocusCardActionBar.tsx | 64 +++++-------------- .../components/post/focus/PostFocusCard.tsx | 1 - 2 files changed, 17 insertions(+), 48 deletions(-) diff --git a/packages/shared/src/components/post/focus/FocusCardActionBar.tsx b/packages/shared/src/components/post/focus/FocusCardActionBar.tsx index de70d8284a..870cea0f9b 100644 --- a/packages/shared/src/components/post/focus/FocusCardActionBar.tsx +++ b/packages/shared/src/components/post/focus/FocusCardActionBar.tsx @@ -9,7 +9,6 @@ import { useBlockPostPanel } from '../../../hooks/post/useBlockPostPanel'; import { useCanAwardUser } from '../../../hooks/useCoresFeature'; import { useLazyModal } from '../../../hooks/useLazyModal'; import { LazyModal } from '../../modals/common/types'; -import { useLayoutVariant } from '../../../hooks/layout/useLayoutVariant'; import { useAuthContext } from '../../../contexts/AuthContext'; import type { PostOrigin } from '../../../hooks/log/useLogContextData'; import { Origin } from '../../../lib/log'; @@ -18,7 +17,6 @@ import { ButtonSize } from '../../buttons/Button'; import { ButtonColor } from '../../buttons/ButtonV2'; import { CardAction } from '../../buttons/CardAction'; import { BookmarkButton } from '../../buttons/BookmarkButton'; -import CloseButton from '../../CloseButton'; import { UpvoteButtonIcon } from '../../cards/common/UpvoteButtonIcon'; import { IconSize } from '../../Icon'; import { @@ -37,27 +35,24 @@ interface FocusCardActionBarProps { origin?: PostOrigin; onComment?: () => void; onCopyLinkClick?: (post?: Post) => void; - /** When provided (post modal), renders an X close button next to the menu. */ - onClose?: () => void; className?: string; } /** * Engagement bar for the redesign focus card, built on the CardAction * primitives (PR #6064 guideline): each action's count lives inside the click - * target so the icon or number performs the action. Sticks to the top while - * scrolling; the modal X appears only once the bar is pinned. + * target so the icon or number performs the action. On tablet up it floats as a + * centred pill pinned to the BOTTOM of the scroll area while reading (never the + * top), matching the reader and mobile bars. */ export const FocusCardActionBar = ({ post, origin = Origin.ArticlePage, onComment, onCopyLinkClick, - onClose, className, }: FocusCardActionBarProps): ReactElement => { const { user, showLogin } = useAuthContext(); - const { isV2 } = useLayoutVariant(); const { toggleUpvote, toggleDownvote } = useVotePost(); const { toggleBookmark } = useBookmarkPost(); const { onShowPanel, onClose: onCloseBlockPanel } = useBlockPostPanel(post); @@ -67,28 +62,20 @@ export const FocusCardActionBar = ({ receivingUser: post.author as LoggedUser | undefined, }); - // Track whether the bar is pinned, and at which edge. The sentinel sits just - // above the bar: when it scrolls above the viewport top the bar is pinned at - // the TOP; when it's still below the viewport the bar is floating at the - // BOTTOM. The modal's X is only useful at the top (where the top nav strip - // has scrolled away) — at the bottom that strip is still on screen. + // The sentinel sits just above the bar; once it leaves the viewport the bar + // has floated away from its resting spot, so surface the counts + menu that + // the (now scrolled-off) stats row and header normally carry. const sentinelRef = useRef(null); const barRef = useRef(null); const copyLinkRef = useRef(null); const [isStuck, setIsStuck] = useState(false); - const [isStuckTop, setIsStuckTop] = useState(false); useEffect(() => { const el = sentinelRef.current; if (!el || typeof IntersectionObserver === 'undefined') { return undefined; } const observer = new IntersectionObserver( - ([entry]) => { - const stuck = !entry.isIntersecting; - setIsStuck(stuck); - const rootTop = entry.rootBounds?.top ?? 0; - setIsStuckTop(stuck && entry.boundingClientRect.top < rootTop); - }, + ([entry]) => setIsStuck(!entry.isIntersecting), { threshold: 0 }, ); observer.observe(el); @@ -109,26 +96,13 @@ export const FocusCardActionBar = ({ // at the bottom on load, where the stats row above has scrolled off. Below // tablet the bar is plain in-flow, so keep it stable (no counts) — that's the // width where toggling on scroll looked like flicker. + // Sticky to the BOTTOM only (tablet up) — the bar floats near the bottom of + // the scroll area while reading and settles into its resting spot once + // scrolled to. It never pins to the top (that fought the header and read as + // odd). Below tablet it stays in-flow; the dedicated mobile bar handles the + // pinned behaviour there. const barFloats = useViewSize(ViewSize.Tablet); const isPinned = isStuck && barFloats; - // The X (modal close) only makes sense when pinned at the top; at the bottom - // the modal's top strip — and its own close — are still on screen. - const isPinnedTop = isStuckTop && barFloats; - // Sticky at BOTH edges (`top` + `bottom`), tablet and up only — on mobile the - // dedicated floating bottom bar already covers this, so the desktop treatment - // is excluded there. While its natural spot is still below the fold the bar - // pins near the bottom (always reachable), scrolls naturally through the - // viewport, then pins near the top once it scrolls above. `top-4`/`bottom-4` - // leave a gap from each edge so the pill reads as floating. The top offset - // also accounts for the top chrome — the modal has no app header; on the post - // page the v2 rail hides the global header on laptop for logged-in users, so - // the bar floats near the top, while the legacy/logged-out layout must clear - // a fixed 4rem header (4rem + 1rem gap = top-20). `onClose` is modal-only. - const railOwnsHeader = isV2 && !!user; - const stickyOffsetClassName = - onClose || railOwnsHeader - ? 'tablet:top-4 tablet:bottom-4' - : 'tablet:top-4 tablet:bottom-4 laptop:top-20'; // Fold copy link out of the row when the bar would overflow, and bring it // back inline when there is room again. Measured against the real available @@ -216,12 +190,11 @@ export const FocusCardActionBar = ({
@@ -317,9 +290,6 @@ export const FocusCardActionBar = ({ buttonSize={ButtonSize.Medium} /> )} - {isPinnedTop && onClose && ( - onClose()} /> - )}
diff --git a/packages/shared/src/components/post/focus/PostFocusCard.tsx b/packages/shared/src/components/post/focus/PostFocusCard.tsx index 07e74b42be..2cf866c147 100644 --- a/packages/shared/src/components/post/focus/PostFocusCard.tsx +++ b/packages/shared/src/components/post/focus/PostFocusCard.tsx @@ -579,7 +579,6 @@ export const PostFocusCard = ({ origin={origin} onComment={scrollToComment} onCopyLinkClick={onCopyPostLink} - onClose={onClose} // Tighten the gap to the stats row above (the column's gap-4 alone // read as too large here). className="-mt-2" From 4225b3f093e21623644adb22aa182ceee02e5c15 Mon Sep 17 00:00:00 2001 From: Tsahi Matsliah Date: Wed, 1 Jul 2026 12:07:20 +0300 Subject: [PATCH 38/53] style(post): full-width engagement bar with evenly spaced actions Drop the centred w-fit pill; the bar is now full width with all actions as direct children spread edge-to-edge (justify-between), so icons read as one evenly spaced, consistent set (all IconSize.Small / ButtonSize.Medium). Co-Authored-By: Claude Opus 4.8 --- .../post/focus/FocusCardActionBar.tsx | 178 +++++++++--------- 1 file changed, 84 insertions(+), 94 deletions(-) diff --git a/packages/shared/src/components/post/focus/FocusCardActionBar.tsx b/packages/shared/src/components/post/focus/FocusCardActionBar.tsx index 870cea0f9b..16fc3bcd6a 100644 --- a/packages/shared/src/components/post/focus/FocusCardActionBar.tsx +++ b/packages/shared/src/components/post/focus/FocusCardActionBar.tsx @@ -91,11 +91,6 @@ export const FocusCardActionBar = ({ const upvotes = post.numUpvotes || 0; const comments = post.numComments || 0; const awards = post.numAwards || 0; - // The bar floats (sticky) from tablet up, so surface the metrics + menu - // whenever it's actually pinned there — including when a long post floats it - // at the bottom on load, where the stats row above has scrolled off. Below - // tablet the bar is plain in-flow, so keep it stable (no counts) — that's the - // width where toggling on scroll looked like flicker. // Sticky to the BOTTOM only (tablet up) — the bar floats near the bottom of // the scroll area while reading and settles into its resting spot once // scrolled to. It never pins to the top (that fought the header and read as @@ -190,107 +185,102 @@ export const FocusCardActionBar = ({
-
- - } - iconPressed={} - count={isPinned ? upvotes : undefined} - pressed={isUpvoteActive} - onClick={onToggleUpvote} - /> - - - } - iconPressed={} - pressed={isDownvoteActive} - onClick={onToggleDownvote} - /> - - + + } + iconPressed={} + count={isPinned ? upvotes : undefined} + pressed={isUpvoteActive} + onClick={onToggleUpvote} + /> + + + } + iconPressed={} + pressed={isDownvoteActive} + onClick={onToggleDownvote} + /> + + + } + iconPressed={} + count={isPinned ? comments : undefined} + pressed={post.commented} + onClick={onComment} + /> + + {canAward && ( + } - iconPressed={} - count={isPinned ? comments : undefined} - pressed={post.commented} - onClick={onComment} + id="award-post-btn" + label="Award" + color={ButtonColor.Cabbage} + icon={} + iconPressed={} + count={isPinned ? awards : undefined} + pressed={isAwarded} + onClick={onGiveAward} /> - {canAward && ( - - } - iconPressed={} - count={isPinned ? awards : undefined} - pressed={isAwarded} - onClick={onGiveAward} - /> - - )} -
- -
- - {/* Bookmark stays — it is the primary save action. Copy link folds + )} + + {/* Bookmark stays — it is the primary save action. Copy link folds out when space is tight (see the overflow effect); the `hidden tablet:flex` classes are only the pre-measurement (SSR) fallback — the effect overrides display once it measures. The "…" menu and analytics now live in the card header / stats row. */} -
- - } - onClick={() => onCopyLinkClick?.(post)} - /> - -
- {post.clickbaitTitleDetected && ( - - )} - {/* While pinned, the article header (which owns the "…" menu) has - scrolled away, so surface the menu here — to the left of the X. */} - {isPinned && ( - + + } + onClick={() => onCopyLinkClick?.(post)} /> - )} +
+ {post.clickbaitTitleDetected && ( + + )} + {/* While pinned, the article header (which owns the "…" menu) has + scrolled away, so surface the menu here — to the left of the X. */} + {isPinned && ( + + )}
); From 0bd2d47de3553e0e53f343af4dd35c28894c7def Mon Sep 17 00:00:00 2001 From: Tsahi Matsliah Date: Wed, 1 Jul 2026 12:30:40 +0300 Subject: [PATCH 39/53] feat(post): YouTube-style engagement pills Restyle the action bar as separate rounded surface pills (like YouTube): - upvote (+ count) and downvote share one pill split by a divider - comment, award and copy link are their own surface pills - copy link shows its label inside; the rest are icon/count only - save keeps the bookmark-reminder dropdown, wrapped as a pill Drop the single translucent bar and the copy-link overflow fold (pills wrap). Co-Authored-By: Claude Opus 4.8 --- .../post/focus/FocusCardActionBar.tsx | 241 ++++++++---------- 1 file changed, 110 insertions(+), 131 deletions(-) diff --git a/packages/shared/src/components/post/focus/FocusCardActionBar.tsx b/packages/shared/src/components/post/focus/FocusCardActionBar.tsx index 16fc3bcd6a..f7b13a23dc 100644 --- a/packages/shared/src/components/post/focus/FocusCardActionBar.tsx +++ b/packages/shared/src/components/post/focus/FocusCardActionBar.tsx @@ -38,12 +38,14 @@ interface FocusCardActionBarProps { className?: string; } +// Shared surface pill for a standalone action (comment, award, copy link). +const PILL = '!rounded-full'; + /** - * Engagement bar for the redesign focus card, built on the CardAction - * primitives (PR #6064 guideline): each action's count lives inside the click - * target so the icon or number performs the action. On tablet up it floats as a - * centred pill pinned to the BOTTOM of the scroll area while reading (never the - * top), matching the reader and mobile bars. + * Engagement bar for the redesign focus card, styled like YouTube's action row: + * each action is its own surface pill (some with a label, some icon-only), the + * vote actions share one pill split by a divider, and pills are separated by + * gaps. On tablet up the row floats pinned to the BOTTOM of the scroll area. */ export const FocusCardActionBar = ({ post, @@ -63,11 +65,9 @@ export const FocusCardActionBar = ({ }); // The sentinel sits just above the bar; once it leaves the viewport the bar - // has floated away from its resting spot, so surface the counts + menu that - // the (now scrolled-off) stats row and header normally carry. + // has floated away from its resting spot, so surface the "…" menu the + // (now scrolled-off) header normally carries. const sentinelRef = useRef(null); - const barRef = useRef(null); - const copyLinkRef = useRef(null); const [isStuck, setIsStuck] = useState(false); useEffect(() => { const el = sentinelRef.current; @@ -85,58 +85,15 @@ export const FocusCardActionBar = ({ const isUpvoteActive = post?.userState?.vote === UserVote.Up; const isDownvoteActive = post?.userState?.vote === UserVote.Down; const isAwarded = !!post?.userState?.awarded; - // Counts are hidden in the resting bar (the stats row sitting right above it - // already shows them) and surface only once the bar is pinned and that row - // has scrolled away. const upvotes = post.numUpvotes || 0; const comments = post.numComments || 0; const awards = post.numAwards || 0; - // Sticky to the BOTTOM only (tablet up) — the bar floats near the bottom of - // the scroll area while reading and settles into its resting spot once - // scrolled to. It never pins to the top (that fought the header and read as - // odd). Below tablet it stays in-flow; the dedicated mobile bar handles the - // pinned behaviour there. + // Sticky to the BOTTOM only (tablet up); it never pins to the top. Below + // tablet the pills stay in-flow. The "…" menu surfaces only once the bar has + // floated away from the header that normally owns it. const barFloats = useViewSize(ViewSize.Tablet); const isPinned = isStuck && barFloats; - // Fold copy link out of the row when the bar would overflow, and bring it - // back inline when there is room again. Measured against the real available - // width — not breakpoints — so it reacts to page/modal resizing. - useEffect(() => { - const bar = barRef.current; - if (!bar) { - return undefined; - } - const fit = () => { - const copyLink = copyLinkRef.current; - // Show first (inline display overrides the SSR fallback classes), then - // hide it if the row still overflows. - if (copyLink) { - copyLink.style.display = 'flex'; - } - const overflows = () => bar.scrollWidth > bar.clientWidth; - if (copyLink && overflows()) { - copyLink.style.display = 'none'; - } - }; - fit(); - if (typeof ResizeObserver === 'undefined') { - return undefined; - } - const observer = new ResizeObserver(fit); - observer.observe(bar); - return () => observer.disconnect(); - // isPinned/counts change the row width (counts + "…" menu appear when pinned). - }, [ - canAward, - post.clickbaitTitleDetected, - post.bookmarked, - isPinned, - upvotes, - comments, - awards, - ]); - const onToggleUpvote = async () => { if (post?.userState?.vote === UserVote.None) { onCloseBlockPanel(true); @@ -183,103 +140,125 @@ export const FocusCardActionBar = ({ <>
- - } - iconPressed={} - count={isPinned ? upvotes : undefined} - pressed={isUpvoteActive} - onClick={onToggleUpvote} - /> - - - } - iconPressed={} - pressed={isDownvoteActive} - onClick={onToggleDownvote} - /> - - - } - iconPressed={} - count={isPinned ? comments : undefined} - pressed={post.commented} - onClick={onComment} - /> - - {canAward && ( - + {/* Vote pill: upvote (+ count) and downvote share one surface pill, + split by a divider — like YouTube's like/dislike. */} +
+ } - iconPressed={} - count={isPinned ? awards : undefined} - pressed={isAwarded} - onClick={onGiveAward} + id="upvote-post-btn" + label="Upvote" + color={ButtonColor.Avocado} + icon={} + iconPressed={} + count={upvotes} + pressed={isUpvoteActive} + onClick={onToggleUpvote} + className={PILL} + /> + +
+ + } + iconPressed={} + pressed={isDownvoteActive} + onClick={onToggleDownvote} + className={PILL} + /> + +
+ +
+ + } + iconPressed={} + count={comments} + pressed={post.commented} + onClick={onComment} + className={PILL} /> +
+ + {canAward && ( +
+ + } + iconPressed={} + count={awards} + pressed={isAwarded} + onClick={onGiveAward} + className={PILL} + /> + +
)} - - {/* Bookmark stays — it is the primary save action. Copy link folds - out when space is tight (see the overflow effect); the - `hidden tablet:flex` classes are only the pre-measurement (SSR) - fallback — the effect overrides display once it measures. The "…" - menu and analytics now live in the card header / stats row. */} -
+ + {/* Save pill — wrapped so the surface shows through; keeps the + bookmark-reminder dropdown. */} +
+ +
+ + {/* Copy link — a labelled pill (text inside, YouTube "Share"-style). */} +
} onClick={() => onCopyLinkClick?.(post)} + className={PILL} />
+ {post.clickbaitTitleDetected && ( )} - {/* While pinned, the article header (which owns the "…" menu) has - scrolled away, so surface the menu here — to the left of the X. */} + + {/* While floated, the header (which owns the "…" menu) has scrolled + away, so surface the menu here as its own pill. */} {isPinned && ( - +
+ +
)}
From 83d55f28bd75b5ce836312f3f20303332b173251 Mon Sep 17 00:00:00 2001 From: Tsahi Matsliah Date: Wed, 1 Jul 2026 12:39:40 +0300 Subject: [PATCH 40/53] fix(post): show the sharer's commentary on share posts Redesign share posts render the shared article (article = post.sharedPost), so the h1 showed the article's title and the user's own commentary (post.title) was dropped. Surface post.title above the shared article when it differs from the article's title. Co-Authored-By: Claude Opus 4.8 --- .../src/components/post/focus/PostFocusCard.tsx | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/packages/shared/src/components/post/focus/PostFocusCard.tsx b/packages/shared/src/components/post/focus/PostFocusCard.tsx index 2cf866c147..2e0bb0fefa 100644 --- a/packages/shared/src/components/post/focus/PostFocusCard.tsx +++ b/packages/shared/src/components/post/focus/PostFocusCard.tsx @@ -231,6 +231,11 @@ export const PostFocusCard = ({ : undefined; const isVideoType = isVideoPost(article); const { title } = useSmartTitle(article); + // A share post carries the user's own commentary in `post.title` (separate + // from the shared article's title). Surface it so the text the user actually + // wrote isn't dropped; skip it when it just mirrors the article's title. + const commentary = + isShared && post.title && post.title !== article.title ? post.title : null; const { onCopyPostLink, onReadArticle } = usePostContent({ origin, post }); const { openModal } = useLazyModal(); const { onShowUpvoted } = useUpvoteQuery(); @@ -407,6 +412,12 @@ export const PostFocusCard = ({ {!isShared && isCollection && (

Collection

)} + {/* The sharer's own words, above the shared article they reference. */} + {commentary && ( +

+ {commentary} +

+ )} {/* Title column and cover image sit side by side. Below tablet the image is a top-aligned square (its original look); from tablet up it stretches to match the title + read button column From bcd29dcde476002bff4e1ff03386f45247656d06 Mon Sep 17 00:00:00 2001 From: Tsahi Matsliah Date: Wed, 1 Jul 2026 13:26:43 +0300 Subject: [PATCH 41/53] style(post): square off engagement pills to match our buttons Drop the oval rounded-full pills for the button radius (rounded-12) plus the standard button border (border-border-subtlest-tertiary) on surface-float, matching our button styling. Inner buttons round to rounded-10 so their hover sits inside the pill border. Co-Authored-By: Claude Opus 4.8 --- .../post/focus/FocusCardActionBar.tsx | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/packages/shared/src/components/post/focus/FocusCardActionBar.tsx b/packages/shared/src/components/post/focus/FocusCardActionBar.tsx index f7b13a23dc..c3b58a514b 100644 --- a/packages/shared/src/components/post/focus/FocusCardActionBar.tsx +++ b/packages/shared/src/components/post/focus/FocusCardActionBar.tsx @@ -38,8 +38,9 @@ interface FocusCardActionBarProps { className?: string; } -// Shared surface pill for a standalone action (comment, award, copy link). -const PILL = '!rounded-full'; +// Inner buttons round slightly tighter than their surrounding pill so the +// hover sits cleanly inside the pill's border. +const PILL = '!rounded-10'; /** * Engagement bar for the redesign focus card, styled like YouTube's action row: @@ -150,7 +151,7 @@ export const FocusCardActionBar = ({ > {/* Vote pill: upvote (+ count) and downvote share one surface pill, split by a divider — like YouTube's like/dislike. */} -
+
-
+
{canAward && ( -
+
@@ -217,7 +218,7 @@ export const FocusCardActionBar = ({ {/* Save pill — wrapped so the surface shows through; keeps the bookmark-reminder dropdown. */} -
+
{/* Copy link — a labelled pill (text inside, YouTube "Share"-style). */} -
+
+
Date: Wed, 1 Jul 2026 13:32:32 +0300 Subject: [PATCH 42/53] fix(post): keep cover at OG ratio on desktop, square on mobile MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A short title squashed the cover because it stretched to the title-column height. Give the cover a fixed ratio instead: square below tablet, the wide open-graph cover ratio (25/13) from tablet up — so it never distorts. Co-Authored-By: Claude Opus 4.8 --- .../components/post/focus/PostFocusCard.tsx | 20 +++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/packages/shared/src/components/post/focus/PostFocusCard.tsx b/packages/shared/src/components/post/focus/PostFocusCard.tsx index 2e0bb0fefa..d8c5b78446 100644 --- a/packages/shared/src/components/post/focus/PostFocusCard.tsx +++ b/packages/shared/src/components/post/focus/PostFocusCard.tsx @@ -418,12 +418,12 @@ export const PostFocusCard = ({ {commentary}

)} - {/* Title column and cover image sit side by side. Below tablet - the image is a top-aligned square (its original look); from - tablet up it stretches to match the title + read button column - height. The cover image opens a lightbox rather than navigating - away. */} -
+ {/* Title column and cover image sit side by side, top-aligned. The + image keeps a fixed ratio (square on mobile/small tablet, the + wide open-graph cover ratio from tablet up) so a short title + can't squash it. The cover opens a lightbox rather than + navigating away. */} +

25/13). Fixed either way so the image + // never distorts; object-cover crops to fit. + className="aspect-square w-full tablet:aspect-[25/13]" fallbackSrc={cloudinaryPostImageCoverPlaceholder} fetchPriority="high" imgAlt="Post cover image" From 3ac657f352c63e0ef3a127a19c302a6d0311d0bd Mon Sep 17 00:00:00 2001 From: Tsahi Matsliah Date: Wed, 1 Jul 2026 13:43:09 +0300 Subject: [PATCH 43/53] feat(post): Save label + Reddit-style vote pill - Bookmark now shows a "Save" / "Saved" label (BookmarkButton respects a caller-provided iconPosition so the label sits beside the icon). - Vote pill: move the score between the up/down arrows (Reddit-style) and drop the divider; the count dims when not upvoted. Co-Authored-By: Claude Opus 4.8 --- .../src/components/buttons/BookmarkButton.tsx | 2 +- .../post/focus/FocusCardActionBar.tsx | 29 ++++++++++++++----- 2 files changed, 22 insertions(+), 9 deletions(-) diff --git a/packages/shared/src/components/buttons/BookmarkButton.tsx b/packages/shared/src/components/buttons/BookmarkButton.tsx index 7536a8fa8c..51de4a3942 100644 --- a/packages/shared/src/components/buttons/BookmarkButton.tsx +++ b/packages/shared/src/components/buttons/BookmarkButton.tsx @@ -102,7 +102,7 @@ export function BookmarkButton({ {...buttonProps} type="button" pressed={post.bookmarked} - iconPosition={buttonIconPosition} + iconPosition={buttonProps.iconPosition ?? buttonIconPosition} onClick={(e: React.MouseEvent) => buttonProps.onClick?.(e) } diff --git a/packages/shared/src/components/post/focus/FocusCardActionBar.tsx b/packages/shared/src/components/post/focus/FocusCardActionBar.tsx index c3b58a514b..bd0f869999 100644 --- a/packages/shared/src/components/post/focus/FocusCardActionBar.tsx +++ b/packages/shared/src/components/post/focus/FocusCardActionBar.tsx @@ -13,10 +13,11 @@ import { useAuthContext } from '../../../contexts/AuthContext'; import type { PostOrigin } from '../../../hooks/log/useLogContextData'; import { Origin } from '../../../lib/log'; import { AuthTriggers } from '../../../lib/auth'; -import { ButtonSize } from '../../buttons/Button'; +import { ButtonSize, ButtonIconPosition } from '../../buttons/Button'; import { ButtonColor } from '../../buttons/ButtonV2'; import { CardAction } from '../../buttons/CardAction'; import { BookmarkButton } from '../../buttons/BookmarkButton'; +import InteractionCounter from '../../InteractionCounter'; import { UpvoteButtonIcon } from '../../cards/common/UpvoteButtonIcon'; import { IconSize } from '../../Icon'; import { @@ -149,8 +150,8 @@ export const FocusCardActionBar = ({ className, )} > - {/* Vote pill: upvote (+ count) and downvote share one surface pill, - split by a divider — like YouTube's like/dislike. */} + {/* Vote pill (Reddit-style): upvote and downvote share one surface pill + with the score sitting between the two arrows. */}
} iconPressed={} - count={upvotes} pressed={isUpvoteActive} onClick={onToggleUpvote} className={PILL} /> -
+ {upvotes > 0 && ( + + + + )} )} - {/* Save pill — wrapped so the surface shows through; keeps the - bookmark-reminder dropdown. */} + {/* Save pill — labelled (Save / Saved), wrapped so the surface shows + through; keeps the bookmark-reminder dropdown. */}
+ > + {post.bookmarked ? 'Saved' : 'Save'} +
{/* Copy link — a labelled pill (text inside, YouTube "Share"-style). */} From d200e0049c65bf23f7ce1420fc974ecd2c7ea166 Mon Sep 17 00:00:00 2001 From: Tsahi Matsliah Date: Wed, 1 Jul 2026 13:56:24 +0300 Subject: [PATCH 44/53] feat(post): split engagement bar into contribution vs utility sides MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Left group: post-contribution actions (vote, comment, award). - Right group: utility actions (copy link, save, three-dots) — the menu now always shows, so the sticky-sentinel/pinned machinery is removed. - Fix the Save label so it matches the other pill labels (muted footnote) instead of the button's default bold/primary text. Co-Authored-By: Claude Opus 4.8 --- .../post/focus/FocusCardActionBar.tsx | 136 ++++++++---------- 1 file changed, 56 insertions(+), 80 deletions(-) diff --git a/packages/shared/src/components/post/focus/FocusCardActionBar.tsx b/packages/shared/src/components/post/focus/FocusCardActionBar.tsx index bd0f869999..6388874f42 100644 --- a/packages/shared/src/components/post/focus/FocusCardActionBar.tsx +++ b/packages/shared/src/components/post/focus/FocusCardActionBar.tsx @@ -1,9 +1,9 @@ import type { ReactElement } from 'react'; -import React, { useEffect, useRef, useState } from 'react'; +import React from 'react'; import classNames from 'classnames'; import type { Post } from '../../../graphql/posts'; import { UserVote } from '../../../graphql/posts'; -import { useViewSize, useVotePost, ViewSize } from '../../../hooks'; +import { useVotePost } from '../../../hooks'; import { useBookmarkPost } from '../../../hooks/useBookmarkPost'; import { useBlockPostPanel } from '../../../hooks/post/useBlockPostPanel'; import { useCanAwardUser } from '../../../hooks/useCoresFeature'; @@ -39,15 +39,20 @@ interface FocusCardActionBarProps { className?: string; } -// Inner buttons round slightly tighter than their surrounding pill so the -// hover sits cleanly inside the pill's border. +// Inner buttons round slightly tighter than their pill so the hover sits +// cleanly inside the border. const PILL = '!rounded-10'; +// Each action sits in a bordered surface pill matching our button styling. +const PILL_WRAP = + 'flex items-center rounded-12 border border-border-subtlest-tertiary bg-surface-float'; +// Match the other pill labels (e.g. "Copy link"): muted, footnote, not bold. +const LABEL = 'font-medium text-text-tertiary typo-footnote'; /** - * Engagement bar for the redesign focus card, styled like YouTube's action row: - * each action is its own surface pill (some with a label, some icon-only), the - * vote actions share one pill split by a divider, and pills are separated by - * gaps. On tablet up the row floats pinned to the BOTTOM of the scroll area. + * Engagement bar for the redesign focus card. Post-contribution actions (vote, + * comment, award) sit on the left; utility actions (copy link, save, menu) on + * the right. Each is a bordered surface pill matching our buttons. On tablet up + * the row floats pinned to the bottom of the scroll area. */ export const FocusCardActionBar = ({ post, @@ -66,35 +71,12 @@ export const FocusCardActionBar = ({ receivingUser: post.author as LoggedUser | undefined, }); - // The sentinel sits just above the bar; once it leaves the viewport the bar - // has floated away from its resting spot, so surface the "…" menu the - // (now scrolled-off) header normally carries. - const sentinelRef = useRef(null); - const [isStuck, setIsStuck] = useState(false); - useEffect(() => { - const el = sentinelRef.current; - if (!el || typeof IntersectionObserver === 'undefined') { - return undefined; - } - const observer = new IntersectionObserver( - ([entry]) => setIsStuck(!entry.isIntersecting), - { threshold: 0 }, - ); - observer.observe(el); - return () => observer.disconnect(); - }, []); - const isUpvoteActive = post?.userState?.vote === UserVote.Up; const isDownvoteActive = post?.userState?.vote === UserVote.Down; const isAwarded = !!post?.userState?.awarded; const upvotes = post.numUpvotes || 0; const comments = post.numComments || 0; const awards = post.numAwards || 0; - // Sticky to the BOTTOM only (tablet up); it never pins to the top. Below - // tablet the pills stay in-flow. The "…" menu surfaces only once the bar has - // floated away from the header that normally owns it. - const barFloats = useViewSize(ViewSize.Tablet); - const isPinned = isStuck && barFloats; const onToggleUpvote = async () => { if (post?.userState?.vote === UserVote.None) { @@ -139,20 +121,17 @@ export const FocusCardActionBar = ({ }; return ( - <> -
-
- {/* Vote pill (Reddit-style): upvote and downvote share one surface pill - with the score sitting between the two arrows. */} -
+
+
+ {/* Vote pill (Reddit-style): the score sits between the arrows. */} +
-
+
{canAward && ( -
+
@@ -225,10 +204,29 @@ export const FocusCardActionBar = ({
)} +
+ +
+ {post.clickbaitTitleDetected && ( + + )} + +
+ + } + onClick={() => onCopyLinkClick?.(post)} + className={PILL} + /> + +
- {/* Save pill — labelled (Save / Saved), wrapped so the surface shows - through; keeps the bookmark-reminder dropdown. */} -
+ {/* Save — keeps the bookmark-reminder dropdown; label styled to match + the other pill labels (not the button's default bold/primary). */} +
- {post.bookmarked ? 'Saved' : 'Save'} + {post.bookmarked ? 'Saved' : 'Save'}
- {/* Copy link — a labelled pill (text inside, YouTube "Share"-style). */} -
- - } - onClick={() => onCopyLinkClick?.(post)} - className={PILL} - /> - +
+
- - {post.clickbaitTitleDetected && ( - - )} - - {/* While floated, the header (which owns the "…" menu) has scrolled - away, so surface the menu here as its own pill. */} - {isPinned && ( -
- -
- )}
- +
); }; From 130b52bbcd5adfb666247b848b5ab611831c643c Mon Sep 17 00:00:00 2001 From: Tsahi Matsliah Date: Wed, 1 Jul 2026 14:30:42 +0300 Subject: [PATCH 45/53] fix(post): Save action as CardAction to stop label clipping The deprecated BookmarkButton (QuaternaryButton) renders its label as a separate bold element that clipped inside the pill. Use a CardAction like Copy link so "Save" / "Saved" sizes and styles consistently. Revert the BookmarkButton iconPosition tweak (no longer used). Co-Authored-By: Claude Opus 4.8 --- .../src/components/buttons/BookmarkButton.tsx | 2 +- .../post/focus/FocusCardActionBar.tsx | 38 +++++++++---------- 2 files changed, 18 insertions(+), 22 deletions(-) diff --git a/packages/shared/src/components/buttons/BookmarkButton.tsx b/packages/shared/src/components/buttons/BookmarkButton.tsx index 51de4a3942..7536a8fa8c 100644 --- a/packages/shared/src/components/buttons/BookmarkButton.tsx +++ b/packages/shared/src/components/buttons/BookmarkButton.tsx @@ -102,7 +102,7 @@ export function BookmarkButton({ {...buttonProps} type="button" pressed={post.bookmarked} - iconPosition={buttonProps.iconPosition ?? buttonIconPosition} + iconPosition={buttonIconPosition} onClick={(e: React.MouseEvent) => buttonProps.onClick?.(e) } diff --git a/packages/shared/src/components/post/focus/FocusCardActionBar.tsx b/packages/shared/src/components/post/focus/FocusCardActionBar.tsx index 6388874f42..9094eed04e 100644 --- a/packages/shared/src/components/post/focus/FocusCardActionBar.tsx +++ b/packages/shared/src/components/post/focus/FocusCardActionBar.tsx @@ -13,14 +13,13 @@ import { useAuthContext } from '../../../contexts/AuthContext'; import type { PostOrigin } from '../../../hooks/log/useLogContextData'; import { Origin } from '../../../lib/log'; import { AuthTriggers } from '../../../lib/auth'; -import { ButtonSize, ButtonIconPosition } from '../../buttons/Button'; +import { ButtonSize } from '../../buttons/Button'; import { ButtonColor } from '../../buttons/ButtonV2'; import { CardAction } from '../../buttons/CardAction'; -import { BookmarkButton } from '../../buttons/BookmarkButton'; import InteractionCounter from '../../InteractionCounter'; import { UpvoteButtonIcon } from '../../cards/common/UpvoteButtonIcon'; -import { IconSize } from '../../Icon'; import { + BookmarkIcon, DiscussIcon as CommentIcon, DownvoteIcon, LinkIcon, @@ -45,8 +44,6 @@ const PILL = '!rounded-10'; // Each action sits in a bordered surface pill matching our button styling. const PILL_WRAP = 'flex items-center rounded-12 border border-border-subtlest-tertiary bg-surface-float'; -// Match the other pill labels (e.g. "Copy link"): muted, footnote, not bold. -const LABEL = 'font-medium text-text-tertiary typo-footnote'; /** * Engagement bar for the redesign focus card. Post-contribution actions (vote, @@ -224,23 +221,22 @@ export const FocusCardActionBar = ({
- {/* Save — keeps the bookmark-reminder dropdown; label styled to match - the other pill labels (not the button's default bold/primary). */} + {/* Save — a CardAction like Copy link so the label sizes and styles + consistently (the deprecated BookmarkButton clipped its label). */}
- - {post.bookmarked ? 'Saved' : 'Save'} - + + } + iconPressed={} + pressed={post.bookmarked} + onClick={onToggleBookmark} + className={PILL} + /> +
From 4bd8ae01bb9cccb0bb11bd12016fa14531765b8b Mon Sep 17 00:00:00 2001 From: Tsahi Matsliah Date: Wed, 1 Jul 2026 14:33:37 +0300 Subject: [PATCH 46/53] =?UTF-8?q?feat(post):=20utility=20side=20=E2=80=94?= =?UTF-8?q?=20drop=20menu,=20swap=20save/copy,=20icon-only=20on=20mobile?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Remove the three-dots menu from the bar (the header already carries it). - Order the utility side Save then Copy link. - Collapse both to icon-only below tablet (label shows from tablet up). Co-Authored-By: Claude Opus 4.8 --- .../post/focus/FocusCardActionBar.tsx | 38 ++++++++----------- 1 file changed, 15 insertions(+), 23 deletions(-) diff --git a/packages/shared/src/components/post/focus/FocusCardActionBar.tsx b/packages/shared/src/components/post/focus/FocusCardActionBar.tsx index 9094eed04e..b45c63d82a 100644 --- a/packages/shared/src/components/post/focus/FocusCardActionBar.tsx +++ b/packages/shared/src/components/post/focus/FocusCardActionBar.tsx @@ -13,7 +13,6 @@ import { useAuthContext } from '../../../contexts/AuthContext'; import type { PostOrigin } from '../../../hooks/log/useLogContextData'; import { Origin } from '../../../lib/log'; import { AuthTriggers } from '../../../lib/auth'; -import { ButtonSize } from '../../buttons/Button'; import { ButtonColor } from '../../buttons/ButtonV2'; import { CardAction } from '../../buttons/CardAction'; import InteractionCounter from '../../InteractionCounter'; @@ -28,7 +27,6 @@ import { import { Tooltip } from '../../tooltip/Tooltip'; import type { LoggedUser } from '../../../lib/user'; import { PostClickbaitShield } from '../common/PostClickbaitShield'; -import { PostMenuOptions } from '../PostMenuOptions'; interface FocusCardActionBarProps { post: Post; @@ -44,6 +42,9 @@ const PILL = '!rounded-10'; // Each action sits in a bordered surface pill matching our button styling. const PILL_WRAP = 'flex items-center rounded-12 border border-border-subtlest-tertiary bg-surface-float'; +// Below tablet, collapse the label to an icon-only button; show it from tablet. +const HIDE_LABEL_MOBILE = + '[&_.card-action-content]:hidden tablet:[&_.card-action-content]:inline-flex'; /** * Engagement bar for the redesign focus card. Post-contribution actions (vote, @@ -208,21 +209,7 @@ export const FocusCardActionBar = ({ )} -
- - } - onClick={() => onCopyLinkClick?.(post)} - className={PILL} - /> - -
- - {/* Save — a CardAction like Copy link so the label sizes and styles - consistently (the deprecated BookmarkButton clipped its label). */} + {/* Save then Copy link. Both collapse to icon-only below tablet. */}
} pressed={post.bookmarked} onClick={onToggleBookmark} - className={PILL} + className={classNames(PILL, HIDE_LABEL_MOBILE)} />
- + + } + onClick={() => onCopyLinkClick?.(post)} + className={classNames(PILL, HIDE_LABEL_MOBILE)} + /> +
From f943336e636376a8a8825dde7131342c819e722e Mon Sep 17 00:00:00 2001 From: Tsahi Matsliah Date: Wed, 1 Jul 2026 14:42:52 +0300 Subject: [PATCH 47/53] fix(post): taller pills; true icon-only Save/Copy on mobile - Add vertical padding to the action pills (py-1). - Drive Save / Copy link label visibility off the tablet breakpoint via useViewSize, so below tablet they render as proper icon-only square buttons (fixes the leftover label space on the right) instead of a CSS-hidden label. Co-Authored-By: Claude Opus 4.8 --- .../post/focus/FocusCardActionBar.tsx | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/packages/shared/src/components/post/focus/FocusCardActionBar.tsx b/packages/shared/src/components/post/focus/FocusCardActionBar.tsx index b45c63d82a..a7896fce8f 100644 --- a/packages/shared/src/components/post/focus/FocusCardActionBar.tsx +++ b/packages/shared/src/components/post/focus/FocusCardActionBar.tsx @@ -3,7 +3,7 @@ import React from 'react'; import classNames from 'classnames'; import type { Post } from '../../../graphql/posts'; import { UserVote } from '../../../graphql/posts'; -import { useVotePost } from '../../../hooks'; +import { useViewSize, useVotePost, ViewSize } from '../../../hooks'; import { useBookmarkPost } from '../../../hooks/useBookmarkPost'; import { useBlockPostPanel } from '../../../hooks/post/useBlockPostPanel'; import { useCanAwardUser } from '../../../hooks/useCoresFeature'; @@ -41,10 +41,7 @@ interface FocusCardActionBarProps { const PILL = '!rounded-10'; // Each action sits in a bordered surface pill matching our button styling. const PILL_WRAP = - 'flex items-center rounded-12 border border-border-subtlest-tertiary bg-surface-float'; -// Below tablet, collapse the label to an icon-only button; show it from tablet. -const HIDE_LABEL_MOBILE = - '[&_.card-action-content]:hidden tablet:[&_.card-action-content]:inline-flex'; + 'flex items-center rounded-12 border border-border-subtlest-tertiary bg-surface-float py-1'; /** * Engagement bar for the redesign focus card. Post-contribution actions (vote, @@ -69,6 +66,9 @@ export const FocusCardActionBar = ({ receivingUser: post.author as LoggedUser | undefined, }); + // Labels show from tablet up; below that Save / Copy link are icon-only so + // they collapse to a clean square button (no leftover label space). + const showLabels = useViewSize(ViewSize.Tablet); const isUpvoteActive = post?.userState?.vote === UserVote.Up; const isDownvoteActive = post?.userState?.vote === UserVote.Down; const isAwarded = !!post?.userState?.awarded; @@ -215,13 +215,13 @@ export const FocusCardActionBar = ({ } iconPressed={} pressed={post.bookmarked} onClick={onToggleBookmark} - className={classNames(PILL, HIDE_LABEL_MOBILE)} + className={PILL} />
@@ -230,11 +230,11 @@ export const FocusCardActionBar = ({ } onClick={() => onCopyLinkClick?.(post)} - className={classNames(PILL, HIDE_LABEL_MOBILE)} + className={PILL} />
From edbc7a16a6f1bd47b08deac14d728a77a9a4a937 Mon Sep 17 00:00:00 2001 From: Tsahi Matsliah Date: Wed, 1 Jul 2026 14:46:08 +0300 Subject: [PATCH 48/53] fix(post): restore the Boost button on the redesign card The boost CTA lived in PostHeaderActions, which the focus card doesn't use, so it vanished from the post page/modal. Surface BoostPostButton in the header row (gated by useShowBoostButton), scoping the three-dots rotate to the menu so the boost icon isn't rotated. Co-Authored-By: Claude Opus 4.8 --- .../components/post/focus/PostFocusCard.tsx | 25 ++++++++++++++----- 1 file changed, 19 insertions(+), 6 deletions(-) diff --git a/packages/shared/src/components/post/focus/PostFocusCard.tsx b/packages/shared/src/components/post/focus/PostFocusCard.tsx index d8c5b78446..74caf08353 100644 --- a/packages/shared/src/components/post/focus/PostFocusCard.tsx +++ b/packages/shared/src/components/post/focus/PostFocusCard.tsx @@ -44,6 +44,8 @@ import { FollowButton } from '../../contentPreference/FollowButton'; import { ContentPreferenceType } from '../../../graphql/contentPreference'; import { PostSidebarAdWidget } from '../PostSidebarAdWidget'; import { PostMenuOptions } from '../PostMenuOptions'; +import { BoostPostButton } from '../../../features/boost/BoostButton'; +import { useShowBoostButton } from '../../../features/boost/useShowBoostButton'; import { FocusCardActionBar } from './FocusCardActionBar'; import { PostDiscussionPanel } from './PostDiscussionPanel'; import { CollectionSources } from './CollectionSources'; @@ -239,6 +241,9 @@ export const PostFocusCard = ({ const { onCopyPostLink, onReadArticle } = usePostContent({ origin, post }); const { openModal } = useLazyModal(); const { onShowUpvoted } = useUpvoteQuery(); + // Boost CTA for eligible authors — lived in PostHeaderActions, which the + // redesign card doesn't use, so surface it here in the header row. + const showBoostButton = useShowBoostButton({ post }); const { onReadClick: onReaderInstallGateClick } = useReaderInstallPromptGate(post); const showCodeSnippets = useFeature(feature.showCodeSnippets); @@ -361,12 +366,20 @@ export const PostFocusCard = ({ /> ) )} -
- +
+ {showBoostButton && ( + + )} +
+ +
From 01993e12b8149be650ae334f17ac10eac0121442 Mon Sep 17 00:00:00 2001 From: Tsahi Matsliah Date: Wed, 1 Jul 2026 14:59:42 +0300 Subject: [PATCH 49/53] feat(post): move date strip directly under the title MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Put the date · read time · source strip back inside the title column, under the title, so a short title pulls it up instead of leaving it below the (taller) cover image. Co-Authored-By: Claude Opus 4.8 --- .../components/post/focus/PostFocusCard.tsx | 55 +++++++++---------- 1 file changed, 27 insertions(+), 28 deletions(-) diff --git a/packages/shared/src/components/post/focus/PostFocusCard.tsx b/packages/shared/src/components/post/focus/PostFocusCard.tsx index 74caf08353..e2d5b7dbc6 100644 --- a/packages/shared/src/components/post/focus/PostFocusCard.tsx +++ b/packages/shared/src/components/post/focus/PostFocusCard.tsx @@ -464,6 +464,33 @@ export const PostFocusCard = ({ title )}

+ {/* Directly under the title so a short title pulls the strip + up (rather than sitting below a taller cover image). */} + 0 && ( + + From{' '} + + {article.domain} + + + ) + } + isVideoType={isVideoType} + readTime={article.readTime} + />
{!isVideoType && article.image && (
- {/* Date, read time and source on a full-width line under the title - row, so its gap to the TL;DR matches the rest of the column. */} - 0 && ( - - From{' '} - - {article.domain} - - - ) - } - isVideoType={isVideoType} - readTime={article.readTime} - /> - {isVideoType && (
Date: Wed, 1 Jul 2026 15:58:34 +0300 Subject: [PATCH 50/53] style(post): space above/below the action bar, not inside the pills MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Revert the pill padding (py-1) and instead add vertical margin (my-2) to the action row, so there's more room above and below the buttons — as intended. Co-Authored-By: Claude Opus 4.8 --- .../shared/src/components/post/focus/FocusCardActionBar.tsx | 2 +- packages/shared/src/components/post/focus/PostFocusCard.tsx | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/shared/src/components/post/focus/FocusCardActionBar.tsx b/packages/shared/src/components/post/focus/FocusCardActionBar.tsx index a7896fce8f..8afa86d272 100644 --- a/packages/shared/src/components/post/focus/FocusCardActionBar.tsx +++ b/packages/shared/src/components/post/focus/FocusCardActionBar.tsx @@ -41,7 +41,7 @@ interface FocusCardActionBarProps { const PILL = '!rounded-10'; // Each action sits in a bordered surface pill matching our button styling. const PILL_WRAP = - 'flex items-center rounded-12 border border-border-subtlest-tertiary bg-surface-float py-1'; + 'flex items-center rounded-12 border border-border-subtlest-tertiary bg-surface-float'; /** * Engagement bar for the redesign focus card. Post-contribution actions (vote, diff --git a/packages/shared/src/components/post/focus/PostFocusCard.tsx b/packages/shared/src/components/post/focus/PostFocusCard.tsx index e2d5b7dbc6..af8c3826c5 100644 --- a/packages/shared/src/components/post/focus/PostFocusCard.tsx +++ b/packages/shared/src/components/post/focus/PostFocusCard.tsx @@ -602,9 +602,9 @@ export const PostFocusCard = ({ origin={origin} onComment={scrollToComment} onCopyLinkClick={onCopyPostLink} - // Tighten the gap to the stats row above (the column's gap-4 alone - // read as too large here). - className="-mt-2" + // Extra breathing room above and below the action row (on top of + // the column's gap-4). + className="my-2" />
From b3548c712851d7c9422fae60793c893ef083905c Mon Sep 17 00:00:00 2001 From: Tsahi Matsliah Date: Wed, 1 Jul 2026 16:09:19 +0300 Subject: [PATCH 51/53] feat(post): group each side of the action bar in one container Combine the actions into two groups: the left (vote + comment + award) now shares a single bordered surface container, matching the right (save + copy) which is also one container. Individual pills are gone. Co-Authored-By: Claude Opus 4.8 --- .../post/focus/FocusCardActionBar.tsx | 190 ++++++++---------- 1 file changed, 89 insertions(+), 101 deletions(-) diff --git a/packages/shared/src/components/post/focus/FocusCardActionBar.tsx b/packages/shared/src/components/post/focus/FocusCardActionBar.tsx index 8afa86d272..aa36948b7f 100644 --- a/packages/shared/src/components/post/focus/FocusCardActionBar.tsx +++ b/packages/shared/src/components/post/focus/FocusCardActionBar.tsx @@ -127,117 +127,105 @@ export const FocusCardActionBar = ({ className, )} > -
- {/* Vote pill (Reddit-style): the score sits between the arrows. */} -
- - } - iconPressed={} - pressed={isUpvoteActive} - onClick={onToggleUpvote} - className={PILL} - /> - - {upvotes > 0 && ( - - - - )} - - } - iconPressed={} - pressed={isDownvoteActive} - onClick={onToggleDownvote} - className={PILL} - /> - -
- -
- + {/* Left group: all post-contribution actions share one container. + Vote is Reddit-style with the score between the arrows. */} +
+ + } + iconPressed={} + pressed={isUpvoteActive} + onClick={onToggleUpvote} + className={PILL} + /> + + {upvotes > 0 && ( + + + + )} + + } + iconPressed={} + pressed={isDownvoteActive} + onClick={onToggleDownvote} + className={PILL} + /> + + + } + iconPressed={} + count={comments} + pressed={post.commented} + onClick={onComment} + className={PILL} + /> + + {canAward && ( + } - iconPressed={} - count={comments} - pressed={post.commented} - onClick={onComment} + id="award-post-btn" + label="Award" + color={ButtonColor.Cabbage} + icon={} + iconPressed={} + count={awards} + pressed={isAwarded} + onClick={onGiveAward} className={PILL} /> -
- - {canAward && ( -
- - } - iconPressed={} - count={awards} - pressed={isAwarded} - onClick={onGiveAward} - className={PILL} - /> - -
)}
-
+ {/* Right group: utility actions share one container. Save then Copy + link; both collapse to icon-only below tablet. */} +
{post.clickbaitTitleDetected && ( )} - - {/* Save then Copy link. Both collapse to icon-only below tablet. */} -
- - } - iconPressed={} - pressed={post.bookmarked} - onClick={onToggleBookmark} - className={PILL} - /> - -
- -
- - } - onClick={() => onCopyLinkClick?.(post)} - className={PILL} - /> - -
+ + } + iconPressed={} + pressed={post.bookmarked} + onClick={onToggleBookmark} + className={PILL} + /> + + + } + onClick={() => onCopyLinkClick?.(post)} + className={PILL} + /> +
); From 5b9e3c5b85fb2d20fb11bf45f764c8ef98abb401 Mon Sep 17 00:00:00 2001 From: Tsahi Matsliah Date: Wed, 1 Jul 2026 17:57:54 +0300 Subject: [PATCH 52/53] style(post): make the two action groups floating glass bars MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Give each group container the macOS/iOS floating-bar treatment — translucent blurred surface + soft shadow — keeping the rounded-12 button radius. Co-Authored-By: Claude Opus 4.8 --- .../shared/src/components/post/focus/FocusCardActionBar.tsx | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/packages/shared/src/components/post/focus/FocusCardActionBar.tsx b/packages/shared/src/components/post/focus/FocusCardActionBar.tsx index aa36948b7f..705fd422d5 100644 --- a/packages/shared/src/components/post/focus/FocusCardActionBar.tsx +++ b/packages/shared/src/components/post/focus/FocusCardActionBar.tsx @@ -39,9 +39,10 @@ interface FocusCardActionBarProps { // Inner buttons round slightly tighter than their pill so the hover sits // cleanly inside the border. const PILL = '!rounded-10'; -// Each action sits in a bordered surface pill matching our button styling. +// Each side is one floating glass bar (macOS/iOS style): a translucent, +// blurred surface with a soft shadow, rounded to match our buttons. const PILL_WRAP = - 'flex items-center rounded-12 border border-border-subtlest-tertiary bg-surface-float'; + 'flex items-center rounded-12 border border-border-subtlest-tertiary bg-surface-float shadow-[0_0.25rem_1.5rem_0_var(--theme-shadow-shadow1)] backdrop-blur-[2.5rem]'; /** * Engagement bar for the redesign focus card. Post-contribution actions (vote, From cd2a34314fbce3dc784c9d81af0e94027cfa78b9 Mon Sep 17 00:00:00 2001 From: Tsahi Matsliah Date: Wed, 1 Jul 2026 18:23:42 +0300 Subject: [PATCH 53/53] style(post): single full-width floating action bar MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Match the production banner: one full-width glass bar (translucent, blurred, soft shadow) with icon actions split left (vote, comment, award) and right (bookmark, copy link, shield) via justify-between — instead of two separate group pills. Keeps the bottom float (no top stickiness). Co-Authored-By: Claude Opus 4.8 --- .../post/focus/FocusCardActionBar.tsx | 70 +++++-------------- 1 file changed, 16 insertions(+), 54 deletions(-) diff --git a/packages/shared/src/components/post/focus/FocusCardActionBar.tsx b/packages/shared/src/components/post/focus/FocusCardActionBar.tsx index 705fd422d5..0fd5c11f5d 100644 --- a/packages/shared/src/components/post/focus/FocusCardActionBar.tsx +++ b/packages/shared/src/components/post/focus/FocusCardActionBar.tsx @@ -3,7 +3,7 @@ import React from 'react'; import classNames from 'classnames'; import type { Post } from '../../../graphql/posts'; import { UserVote } from '../../../graphql/posts'; -import { useViewSize, useVotePost, ViewSize } from '../../../hooks'; +import { useVotePost } from '../../../hooks'; import { useBookmarkPost } from '../../../hooks/useBookmarkPost'; import { useBlockPostPanel } from '../../../hooks/post/useBlockPostPanel'; import { useCanAwardUser } from '../../../hooks/useCoresFeature'; @@ -15,7 +15,6 @@ import { Origin } from '../../../lib/log'; import { AuthTriggers } from '../../../lib/auth'; import { ButtonColor } from '../../buttons/ButtonV2'; import { CardAction } from '../../buttons/CardAction'; -import InteractionCounter from '../../InteractionCounter'; import { UpvoteButtonIcon } from '../../cards/common/UpvoteButtonIcon'; import { BookmarkIcon, @@ -36,19 +35,11 @@ interface FocusCardActionBarProps { className?: string; } -// Inner buttons round slightly tighter than their pill so the hover sits -// cleanly inside the border. -const PILL = '!rounded-10'; -// Each side is one floating glass bar (macOS/iOS style): a translucent, -// blurred surface with a soft shadow, rounded to match our buttons. -const PILL_WRAP = - 'flex items-center rounded-12 border border-border-subtlest-tertiary bg-surface-float shadow-[0_0.25rem_1.5rem_0_var(--theme-shadow-shadow1)] backdrop-blur-[2.5rem]'; - /** - * Engagement bar for the redesign focus card. Post-contribution actions (vote, - * comment, award) sit on the left; utility actions (copy link, save, menu) on - * the right. Each is a bordered surface pill matching our buttons. On tablet up - * the row floats pinned to the bottom of the scroll area. + * Engagement bar for the redesign focus card: one full-width floating glass bar + * (translucent, blurred, soft shadow) with post-contribution actions on the + * left and utility actions on the right. Floats pinned to the BOTTOM of the + * scroll area on tablet up (never the top). */ export const FocusCardActionBar = ({ post, @@ -67,15 +58,9 @@ export const FocusCardActionBar = ({ receivingUser: post.author as LoggedUser | undefined, }); - // Labels show from tablet up; below that Save / Copy link are icon-only so - // they collapse to a clean square button (no leftover label space). - const showLabels = useViewSize(ViewSize.Tablet); const isUpvoteActive = post?.userState?.vote === UserVote.Up; const isDownvoteActive = post?.userState?.vote === UserVote.Down; const isAwarded = !!post?.userState?.awarded; - const upvotes = post.numUpvotes || 0; - const comments = post.numComments || 0; - const awards = post.numAwards || 0; const onToggleUpvote = async () => { if (post?.userState?.vote === UserVote.None) { @@ -122,15 +107,15 @@ export const FocusCardActionBar = ({ return (
- {/* Left group: all post-contribution actions share one container. - Vote is Reddit-style with the score between the arrows. */} -
+
} pressed={isUpvoteActive} onClick={onToggleUpvote} - className={PILL} /> - {upvotes > 0 && ( - - - - )} } pressed={isDownvoteActive} onClick={onToggleDownvote} - className={PILL} /> @@ -173,10 +145,8 @@ export const FocusCardActionBar = ({ color={ButtonColor.BlueCheese} icon={} iconPressed={} - count={comments} pressed={post.commented} onClick={onComment} - className={PILL} /> {canAward && ( @@ -189,44 +159,36 @@ export const FocusCardActionBar = ({ color={ButtonColor.Cabbage} icon={} iconPressed={} - count={awards} pressed={isAwarded} onClick={onGiveAward} - className={PILL} /> )}
- {/* Right group: utility actions share one container. Save then Copy - link; both collapse to icon-only below tablet. */} -
- {post.clickbaitTitleDetected && ( - - )} +
} iconPressed={} pressed={post.bookmarked} onClick={onToggleBookmark} - className={PILL} /> } onClick={() => onCopyLinkClick?.(post)} - className={PILL} /> + {post.clickbaitTitleDetected && ( + + )}
);