- {post.collectionSources?.length > 0 && (
+ {collectionSources.length > 0 && (
diff --git a/packages/shared/src/components/share/ShareActions.spec.tsx b/packages/shared/src/components/share/ShareActions.spec.tsx
index 5a86a8de95c..69e812b2ccb 100644
--- a/packages/shared/src/components/share/ShareActions.spec.tsx
+++ b/packages/shared/src/components/share/ShareActions.spec.tsx
@@ -70,7 +70,7 @@ describe('ShareActions icon variant on mobile', () => {
it('copies on a single tap when native share is unavailable', async () => {
renderComponent();
- const trigger = screen.getByLabelText('Copy link');
+ const trigger = screen.getByLabelText('Share');
await act(async () => {
fireEvent.click(trigger);
});
diff --git a/packages/shared/src/components/share/ShareActions.tsx b/packages/shared/src/components/share/ShareActions.tsx
index 50ee53e30e8..65581fa1e46 100644
--- a/packages/shared/src/components/share/ShareActions.tsx
+++ b/packages/shared/src/components/share/ShareActions.tsx
@@ -5,9 +5,8 @@ import { Popover, PopoverTrigger } from '@radix-ui/react-popover';
import { PopoverContent } from '../popover/Popover';
import { SocialShareList } from '../widgets/SocialShareList';
import { Button, ButtonSize, ButtonVariant } from '../buttons/Button';
-import { CopyIcon } from '../icons';
+import { ShareIcon } from '../icons';
import { Tooltip } from '../tooltip/Tooltip';
-import { Typography, TypographyType } from '../typography/Typography';
import { useViewSize, ViewSize } from '../../hooks/useViewSize';
import { useShareOrCopyLink } from '../../hooks/useShareOrCopyLink';
import { shouldUseNativeShare } from '../../lib/func';
@@ -26,7 +25,13 @@ export interface ShareActionsProps {
openOnHover?: boolean;
buttonVariant?: ButtonVariant;
buttonSize?: ButtonSize;
- /** Tooltip + accessible label for the icon-only trigger. */
+ /**
+ * Tooltip + accessible label for the icon-only trigger, which always renders
+ * the share arrow: the arrow means "opens a share surface" (social popover on
+ * desktop, native sheet on mobile), while a link/copy glyph is reserved for
+ * controls that copy straight to the clipboard. Keep the two distinct — a
+ * copy glyph here reads as a one-tap copy and misleads.
+ */
label?: string;
emailTitle?: string;
emailSummary?: string;
@@ -45,7 +50,7 @@ export function ShareActions({
openOnHover = false,
buttonVariant = ButtonVariant.Tertiary,
buttonSize = ButtonSize.Small,
- label = 'Copy link',
+ label = 'Share',
emailTitle,
emailSummary,
className,
@@ -92,7 +97,7 @@ export function ShareActions({
type="button"
variant={buttonVariant}
size={buttonSize}
- icon={
}
+ icon={
}
aria-label={label}
className={className}
onClick={() => {
@@ -136,7 +141,7 @@ export function ShareActions({
type="button"
variant={buttonVariant}
size={buttonSize}
- icon={
}
+ icon={
}
aria-label={label}
pressed={open}
className={className}
@@ -144,16 +149,19 @@ export function ShareActions({
/>
+ {/* A 4-column grid at `w-fit` rather than a fixed-width wrapping flex
+ row: the tiles are a fixed `w-16`, so a fixed width left slack that
+ `justify-center` split into uneven side gutters, and a short final
+ row floated to the middle. This hugs the tiles exactly, so the
+ padding is equal on all four sides and every row starts at the left
+ edge. No heading — the trigger and the tiles already say "share". */}
-
- Share
-
{list}
diff --git a/packages/shared/src/hooks/useCopyFeedback.ts b/packages/shared/src/hooks/useCopyFeedback.ts
new file mode 100644
index 00000000000..5444c60fd38
--- /dev/null
+++ b/packages/shared/src/hooks/useCopyFeedback.ts
@@ -0,0 +1,33 @@
+import { useCallback, useEffect, useRef, useState } from 'react';
+
+// Matches the flash `useCopy` already uses for its own `copying` flag, so every
+// copy control across a surface confirms for the same beat.
+const COPY_FEEDBACK_MS = 1000;
+
+export type MarkCopied = (key?: string) => void;
+
+/**
+ * Tracks *which* copy control last fired, so it can swap to a success
+ * checkmark. `useCopy` exposes a `copying` flag already, but there is one per
+ * hook instance — a surface with several copy controls sharing a hook would
+ * light them all up at once.
+ */
+export const useCopyFeedback = (
+ duration = COPY_FEEDBACK_MS,
+): [string | null, MarkCopied] => {
+ const [copiedKey, setCopiedKey] = useState
(null);
+ const timeout = useRef>();
+
+ useEffect(() => () => clearTimeout(timeout.current), []);
+
+ const markCopied = useCallback(
+ (key = 'default') => {
+ clearTimeout(timeout.current);
+ setCopiedKey(key);
+ timeout.current = setTimeout(() => setCopiedKey(null), duration);
+ },
+ [duration],
+ );
+
+ return [copiedKey, markCopied];
+};
diff --git a/packages/shared/src/hooks/useShareBriefingDigest.ts b/packages/shared/src/hooks/useShareBriefingDigest.ts
new file mode 100644
index 00000000000..0686075d4c9
--- /dev/null
+++ b/packages/shared/src/hooks/useShareBriefingDigest.ts
@@ -0,0 +1,16 @@
+import { featureShareBriefingDigest } from '../lib/featureManagement';
+import { useConditionalFeature } from './useConditionalFeature';
+import { useSharingVisibility } from './useSharingVisibility';
+
+// Per-topic gate for the briefing/digest sharing surfaces. Requires the master
+// `sharing_visibility` kill-switch to be on as well, so the initiative can be
+// disabled wholesale without touching this flag.
+export const useShareBriefingDigest = (shouldEvaluate = true): boolean => {
+ const { isEnabled: isSharingVisible } = useSharingVisibility(shouldEvaluate);
+ const { value } = useConditionalFeature({
+ feature: featureShareBriefingDigest,
+ shouldEvaluate: shouldEvaluate && isSharingVisible,
+ });
+
+ return isSharingVisible && value;
+};
diff --git a/packages/shared/src/lib/featureManagement.ts b/packages/shared/src/lib/featureManagement.ts
index 97b38cbdd2b..4dce8c2b4a3 100644
--- a/packages/shared/src/lib/featureManagement.ts
+++ b/packages/shared/src/lib/featureManagement.ts
@@ -308,3 +308,13 @@ export const featureSharingVisibility = new Feature(
// high-traffic icon, so it ramps on its own flag to watch share/copy metrics.
// Keep the default `false` (control = existing `LinkIcon`).
export const featureShareCopyIcon = new Feature('share_copy_icon', false);
+
+// Sharing affordances on the presidential briefing and the personalized digest
+// post: per-item copy actions on the briefing list, a full share popover on the
+// briefing header, and share parity for the digest post (which ships with no
+// share affordance today). Also gated by `sharing_visibility`. Keep the default
+// `false` — GrowthBook ramps it.
+export const featureShareBriefingDigest = new Feature(
+ 'share_briefing_digest',
+ false,
+);
diff --git a/packages/shared/src/lib/share.ts b/packages/shared/src/lib/share.ts
index b6bb78125b7..3651f7adeb2 100644
--- a/packages/shared/src/lib/share.ts
+++ b/packages/shared/src/lib/share.ts
@@ -10,6 +10,7 @@ export enum ShareProvider {
LinkedIn = 'linkedin',
Telegram = 'telegram',
Email = 'email',
+ CopyText = 'copy text',
}
export const getWhatsappShareLink = (link: string): string =>
diff --git a/packages/storybook/package.json b/packages/storybook/package.json
index 7b92acf33af..7b0e12de6ba 100644
--- a/packages/storybook/package.json
+++ b/packages/storybook/package.json
@@ -11,6 +11,7 @@
"prettier": "@dailydotdev/prettier-config",
"dependencies": {
"@dailydotdev/shared": "workspace:*",
+ "@growthbook/growthbook-react": "^0.17.0",
"@tanstack/react-query": "^5.82.0",
"date-fns": "^2.30.0",
"dompurify": "^3.4.0",
diff --git a/packages/storybook/stories/components/BriefSharing.stories.tsx b/packages/storybook/stories/components/BriefSharing.stories.tsx
new file mode 100644
index 00000000000..cebdfdec7a3
--- /dev/null
+++ b/packages/storybook/stories/components/BriefSharing.stories.tsx
@@ -0,0 +1,1048 @@
+import type { Meta, StoryObj } from '@storybook/react-vite';
+import type { ReactNode } from 'react';
+import React, { useMemo } from 'react';
+import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
+import { GrowthBookContext } from '@growthbook/growthbook-react';
+import { BriefContent } from '@dailydotdev/shared/src/components/brief/BriefContent';
+import { BriefListItem } from '@dailydotdev/shared/src/components/brief/BriefListItem';
+import { BriefPostHeaderActions } from '@dailydotdev/shared/src/components/post/brief/BriefPostHeaderActions';
+import { BriefPostHeader } from '@dailydotdev/shared/src/features/briefing/components/BriefPostHeader';
+import { Pill } from '@dailydotdev/shared/src/components/Pill';
+import Toast from '@dailydotdev/shared/src/components/notifications/Toast';
+import {
+ AnalyticsIcon,
+ TimerIcon,
+} from '@dailydotdev/shared/src/components/icons';
+import { FeaturesReadyContext } from '@dailydotdev/shared/src/components/GrowthBookProvider';
+import { getLogContextStatic } from '@dailydotdev/shared/src/contexts/LogContext';
+import AuthContext from '@dailydotdev/shared/src/contexts/AuthContext';
+import SettingsContext from '@dailydotdev/shared/src/contexts/SettingsContext';
+import type { SettingsContextData } from '@dailydotdev/shared/src/contexts/SettingsContext';
+import {
+ ButtonSize,
+ ButtonVariant,
+} from '@dailydotdev/shared/src/components/buttons/Button';
+import {
+ Typography,
+ TypographyColor,
+ TypographyType,
+} from '@dailydotdev/shared/src/components/typography/Typography';
+import {
+ generateQueryKey,
+ RequestKey,
+} from '@dailydotdev/shared/src/lib/query';
+import type { LoggedUser } from '@dailydotdev/shared/src/lib/user';
+import type { Post } from '@dailydotdev/shared/src/graphql/posts';
+import { Origin, TargetId } from '@dailydotdev/shared/src/lib/log';
+import { expect, fn, userEvent, waitFor, within } from 'storybook/test';
+
+/* -------------------------------------------------------------------------- */
+/* Fixtures */
+/* -------------------------------------------------------------------------- */
+
+const briefPost = {
+ id: 'brief-1',
+ slug: 'presidential-briefing-jul-22',
+ title: 'Presidential briefing',
+ summary:
+ 'Rust 1.90 lands async closures, Node 24 ships a stable permission model, and three CVEs hit the npm supply chain.',
+ commentsPermalink: 'https://app.daily.dev/posts/presidential-briefing-jul-22',
+ createdAt: '2026-07-22T06:00:00.000Z',
+ readTime: 4,
+ read: false,
+ flags: { posts: 42, sources: 18 },
+} as unknown as Post;
+
+const digestPost = {
+ ...briefPost,
+ id: 'digest-1',
+ slug: 'your-personalized-digest-jul-22',
+ title: 'Your personalized digest',
+ commentsPermalink:
+ 'https://app.daily.dev/posts/your-personalized-digest-jul-22',
+} as unknown as Post;
+
+const briefWithoutSummary = {
+ ...briefPost,
+ id: 'brief-no-summary',
+ summary: undefined,
+} as unknown as Post;
+
+const briefWithLongTitle = {
+ ...briefPost,
+ id: 'brief-long-title',
+ title:
+ 'Presidential briefing — the npm supply chain incident, Rust 1.90 async closures, and everything else that shipped this week',
+} as unknown as Post;
+
+const mockUser = {
+ id: '1',
+ name: 'Test User',
+ username: 'testuser',
+ email: 'test@example.com',
+ image: 'https://daily-now-res.cloudinary.com/image/upload/placeholder.jpg',
+ providers: ['google'],
+ createdAt: '2024-01-01T00:00:00.000Z',
+ permalink: 'https://daily.dev/testuser',
+} as unknown as LoggedUser;
+
+/* -------------------------------------------------------------------------- */
+/* Feature-flag pinning */
+/* -------------------------------------------------------------------------- */
+
+/**
+ * `useConditionalFeature` reads GrowthBook through `FeaturesReadyContext` +
+ * `GrowthBookContext`. With neither provider mounted (Storybook's default) every
+ * flag resolves to its `defaultValue`, i.e. OFF — so flag-on states have to be
+ * pinned explicitly. This provider does that per story instead of relying on the
+ * mocked `useFeature`, which returns the string `'control'` (truthy) for
+ * everything and therefore can't express an off state.
+ */
+type FlagMap = Record;
+
+const FLAGS_ON: FlagMap = {
+ sharing_visibility: true,
+ share_briefing_digest: true,
+};
+
+const FLAGS_OFF: FlagMap = {
+ sharing_visibility: false,
+ share_briefing_digest: false,
+};
+
+const FeatureGate = ({
+ flags,
+ children,
+}: {
+ flags: FlagMap;
+ children: ReactNode;
+}) => {
+ const growthbook = useMemo(
+ () => ({
+ getFeatureValue: (id: string, fallback: unknown) => flags[id] ?? fallback,
+ }),
+ [flags],
+ );
+
+ return (
+
+
+ (flags[feature.id] ?? feature.defaultValue) as never,
+ }}
+ >
+ {children}
+
+
+ );
+};
+
+/* -------------------------------------------------------------------------- */
+/* Providers */
+/* -------------------------------------------------------------------------- */
+
+const settings = {
+ loadedSettings: true,
+ optOutReadingStreak: false,
+ insaneMode: false,
+ spaciness: 'eco',
+ openNewTab: true,
+ sidebarExpanded: false,
+ flags: {},
+} as unknown as SettingsContextData;
+
+const withProviders = (Story: () => JSX.Element) => {
+ const queryClient = new QueryClient({
+ defaultOptions: { queries: { retry: false, staleTime: Infinity } },
+ });
+ // Mock the short-URL resolution so copy actions don't hit network.
+ queryClient.setQueryData(['shortUrl'], 'https://dly.to/abc123');
+ // `useOnPostClick` pulls in the reading streak query; seed it so the list
+ // stories don't fire a request the Storybook environment can't answer.
+ queryClient.setQueryData(generateQueryKey(RequestKey.UserStreak, mockUser), {
+ max: 0,
+ total: 0,
+ current: 0,
+ weekStart: 0,
+ });
+
+ const LogContext = getLogContextStatic();
+
+ return (
+
+
+
+ false,
+ }}
+ >
+
+
+
+ {/* Every copy action toasts through `useCopy`. Storybook otherwise
+ never mounts the renderer, so the confirmation was invisible
+ here even though it fires in the app. */}
+
+
+
+
+
+ );
+};
+
+/* -------------------------------------------------------------------------- */
+/* Review helpers */
+/* -------------------------------------------------------------------------- */
+
+const Section = ({
+ title,
+ note,
+ children,
+}: {
+ title: string;
+ note?: string;
+ children: ReactNode;
+}) => (
+
+
+ {title}
+
+ {!!note && (
+
+ {note}
+
+ )}
+ {children}
+
+);
+
+/** Opens the share popover so the reviewer sees it without interacting. */
+const openSharePopover = async ({
+ canvasElement,
+}: {
+ canvasElement: HTMLElement;
+}) =>
+ waitFor(async () => {
+ const [trigger] = within(canvasElement).getAllByLabelText('Share briefing');
+ await userEvent.click(trigger);
+ await expect(
+ within(document.body).getByText('WhatsApp'),
+ ).toBeInTheDocument();
+ });
+
+const listItemProps = {
+ origin: Origin.BriefPage,
+ targetId: TargetId.List,
+} as const;
+
+/* -------------------------------------------------------------------------- */
+/* Briefing body fixture */
+/* -------------------------------------------------------------------------- */
+
+// Shaped like a real briefing: h2 sections, a lead paragraph, and bullet lists.
+const briefContentHtml = `
+The npm supply chain took another hit
+Three packages with a combined 40M weekly downloads shipped a post-install script that exfiltrated environment variables. All three were pulled within six hours.
+
+ - What happened: a maintainer account with no 2FA was taken over via a reused password from an unrelated breach.
+ - Who is affected: anyone who ran a fresh install between Tuesday 02:00 and 08:00 UTC. Lockfile-only installs were not hit.
+ - What to do: rotate any credentials that were present in CI during that window, then re-run your install from a clean cache.
+
+Rust 1.90 lands async closures
+The feature has been in nightly for two years. Stabilizing it removes most of the boilerplate around passing async callbacks into combinators.
+
+ - Async closures now capture by reference, which removes a whole class of lifetime workarounds.
+ - The
AsyncFn trait family is stable alongside it, so libraries can finally accept them in public APIs.
+
+Node 24 ships a stable permission model
+Deno-style permission flags are no longer experimental. Filesystem, network, and child-process access can each be denied at startup.
+
+ - Opt-in only — existing apps keep full access until you pass the flags.
+ - Worth wiring into CI first, where the blast radius of a compromised dependency is largest.
+
+`;
+
+/* -------------------------------------------------------------------------- */
+/* Meta */
+/* -------------------------------------------------------------------------- */
+
+const meta: Meta = {
+ title: 'Components/Share/BriefSharing',
+ decorators: [withProviders],
+ parameters: {
+ docs: {
+ description: {
+ component:
+ 'Every state of the briefing / digest sharing surfaces: the briefing-list row, the post header (brief + personalized digest) and the briefing body itself, each in flag-on and flag-off form.',
+ },
+ },
+ },
+};
+
+export default meta;
+
+type ListStory = StoryObj;
+
+/* ========================================================================== */
+/* 1. Row controls */
+/* ========================================================================== */
+
+/**
+ * The pair every surface uses: copy link, then the arrow that opens the social
+ * popover. The row used to group these behind a dropdown, but with no summary
+ * the menu collapsed to two near-identical "copy" entries — more chrome than
+ * the controls it hid.
+ */
+export const RowControls: ListStory = {
+ render: () => (
+
+ ),
+};
+
+/** The social popover itself — no heading, tiles left-aligned, even padding. */
+export const SharePopover: ListStory = {
+ render: () => (
+
+
+
+
+
+ ),
+ play: openSharePopover,
+};
+
+/* ========================================================================== */
+/* 2. Briefing list row */
+/* ========================================================================== */
+
+// Flag on: the row gains the copy menu, pinned via `showCopyActions`.
+export const ListItemWithCopyActions: ListStory = {
+ render: () => (
+
+ ),
+};
+
+// The row with its share popover open, over the full-bleed `CardLink` overlay —
+// this is the stacking case the `z-1` wrapper exists for.
+export const ListItemSharePopoverOpen: ListStory = {
+ render: () => (
+
+ ),
+ play: openSharePopover,
+};
+
+// Flag-off control: the row renders exactly as it does on main today.
+// `showCopyActions` is pinned explicitly rather than left to the gate, so the
+// story stays honest regardless of how Storybook resolves flags.
+export const ListItemControl: ListStory = {
+ render: () => (
+
+ ),
+};
+
+// Read state — title and gradient icon dim, the copy menu stays available.
+export const ListItemRead: ListStory = {
+ render: () => (
+
+ ),
+};
+
+// Locked (non-Plus) briefing — the lock badge and the copy menu coexist.
+export const ListItemLocked: ListStory = {
+ render: () => (
+
+ ),
+};
+
+// Long title with the menu present: the text column is `min-w-0` so the
+// metadata line truncates instead of pushing the control off the row.
+export const ListItemLongTitle: ListStory = {
+ render: () => (
+
+ ),
+};
+
+// Missing metadata: no read time, no counts — the row falls back to
+// "Based on 0 posts from 0 sources" and the menu is unaffected.
+export const ListItemMissingMetadata: ListStory = {
+ render: () => (
+
+ ),
+};
+
+// Mobile: below `mobileXL` the gradient icon is hidden, so the copy menu is the
+// only control competing with the title for horizontal space.
+export const ListItemMobile: ListStory = {
+ parameters: { viewport: { defaultViewport: 'mobile1' } },
+ render: () => (
+
+
+
+ ),
+};
+
+/**
+ * Regression guard for the row overflow.
+ *
+ * `base.css` sets a global `* { flex-shrink: 0 }`, so the text column's old
+ * `w-full` made it refuse to shrink and pushed the trailing control past the
+ * card border — on every row, not just long titles. The column is now
+ * `min-w-0 flex-1`, and the title truncates. Narrow the canvas: the control
+ * should stay inside the border at every width.
+ */
+export const RowShrinkBehaviour: ListStory = {
+ render: () => (
+
+ {[640, 480, 360].map((width) => (
+
+ ))}
+
+ ),
+};
+
+/**
+ * The glyph vocabulary these surfaces share. One meaning per icon:
+ * arrow = opens a share surface, link = copies the bare URL, stacked squares =
+ * copies text with the link appended, page = copies the generated summary.
+ */
+export const IconLanguage: ListStory = {
+ render: () => (
+
+
+
+
+
+
+
Section heading- A single bullet, copied with the briefing link appended.
`}
+ showItemActions
+ />
+
+
+
+
+ ),
+};
+
+/* ========================================================================== */
+/* Inside the briefing post */
+/* ========================================================================== */
+
+/** The briefing page chrome, so the header controls are seen in context. */
+const BriefingPage = ({
+ showItemActions,
+ post = briefPost,
+}: {
+ showItemActions?: boolean;
+ post?: Post;
+}) => (
+
+
+
+
+
+
+
+ 4m read
+
+ }
+ />
+
+ 18 Sources
+
+
+
+
+);
+
+/**
+ * The whole briefing as a reader sees it, flag on: copy-link + share arrow in
+ * the header, and a copy-text control on every section heading and every bullet
+ * (the item's text with the briefing link appended, so a paste is quotable and
+ * still attributed). The per-item controls are revealed on hover of their own
+ * item, so the briefing reads as prose until you reach for one. Touch has no
+ * hover, so coarse pointers get them outright.
+ */
+export const InsideBriefing: ListStory = {
+ render: () => (
+
+
+
+ ),
+};
+
+/** Same briefing with every per-item control forced visible, for review. */
+export const InsideBriefingItemsRevealed: ListStory = {
+ parameters: {
+ pseudo: { hover: true },
+ },
+ render: () => (
+
+
+
+
+
+ ),
+};
+
+/** Flag off — the briefing body is exactly what main renders today. */
+export const InsideBriefingControl: ListStory = {
+ render: () => (
+
+
+
+ ),
+};
+
+// The `/briefing` page as a reviewer would actually see it: newest unread at the
+// top, older reads below, one locked demo row.
+export const BriefingList: ListStory = {
+ render: () => (
+
+
+
+
+
+
+ ),
+};
+
+/* ========================================================================== */
+/* 3. Post header (brief + digest) */
+/* ========================================================================== */
+
+const headerProps = {
+ contextMenuId: 'brief-header-actions',
+ origin: Origin.ArticlePage,
+} as const;
+
+/**
+ * The header reads the gate directly (no override prop), so these stories pin
+ * `share_briefing_digest` through `FeatureGate`.
+ */
+export const PostHeaderShareEnabled: StoryObj = {
+ render: () => (
+
+
+
+
+
+ ),
+};
+
+// Flag off: the legacy desktop-only copy-link button, exactly as main ships it.
+// Below the `laptop` breakpoint this row shows nothing but the container.
+export const PostHeaderControl: StoryObj = {
+ render: () => (
+
+
+
+
+
+ ),
+};
+
+// `showShareButton` omitted — the settings cog only. This is what the digest
+// post renders today, before the parity fix.
+export const PostHeaderWithoutShare: StoryObj = {
+ render: () => (
+
+
+
+
+
+ ),
+};
+
+// The share popover opened from the header trigger (desktop path). On mobile the
+// same trigger taps straight through to the native share sheet.
+export const PostHeaderSharePopover: StoryObj = {
+ render: () => (
+
+
+
+
+
+ ),
+ play: async ({ canvasElement }) => {
+ const trigger = within(canvasElement).getByLabelText('Share briefing');
+ await userEvent.click(trigger);
+ await waitFor(() =>
+ expect(within(document.body).getByText(/copy/i)).toBeInTheDocument(),
+ );
+ },
+};
+
+// Digest parity: the brief and the digest render the same header component. The
+// fix is that the digest now passes `showShareButton` too — both rows should
+// look identical.
+export const DigestParity: StoryObj = {
+ render: () => (
+
+
+
+
+
+ {briefPost.title}
+
+
+
+
+
+
+
+ {digestPost.title}
+
+
+
+
+
+
+ ),
+};
+
+/* ========================================================================== */
+/* 4. Overview — every surface, flag on vs flag off */
+/* ========================================================================== */
+
+export const Overview: ListStory = {
+ parameters: { layout: 'fullscreen' },
+ render: () => (
+
+
+
+
+
+
+
+
+
+
+
+
+ flag ON
+
+
+
+
+
+
+
+ flag OFF (control)
+
+
+
+
+
+
+
+
+
+
+
+
+ after (flag ON)
+
+
+
+
+
+
+
+ before (today on main)
+
+
+
+
+
+
+
+
+
+ ),
+};
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index f5e1cf35fd3..c54e138bcf4 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -725,6 +725,9 @@ importers:
'@dailydotdev/shared':
specifier: workspace:*
version: link:../shared
+ '@growthbook/growthbook-react':
+ specifier: ^0.17.0
+ version: 0.17.0(react@18.3.1)
'@tanstack/react-query':
specifier: ^5.82.0
version: 5.82.0(react@18.3.1)